当前位置: 首页 > news >正文

西电25年A测 语音识别机械臂方案与教程

A测语音识别机械臂攻略

大家好啊,这里是 超级电鼠( 划掉),其实是基本操作啊。

这次的西电老东西A测不讲五德的更换了题目,而网上现在又没有合适的攻略ψ(`∇´)ψ而电鼠又在贴吧立了flag ,所以让我们话不多说,直接开始吧。

注意,此教程请配合整理出的资料包使用…(资料大部分是已经提供的,不过有一个部分是串口)(点开我的资源可见)

如果对你有帮助的话,还请点赞,收藏,关注。这会让我开心一整天~ ୧⍢⃝୨

写在前面的常识

这里是科普常识的小节(作为博主的习惯了,不吐不快)

拿到装备箱,可以看到里面的内容物:

  • 机械臂+STM开发板 * 1
  • ARDUINO UNO R3 开发板 * 1
  • 语音识别模块 * 1
  • 电源转换器 * 1
  • 杜邦线 * 8
  • 数据连接线 * 1

至于软件资料和教程,也在网盘里面有。但是不得不说,这个机械臂的教程真是抽象,感觉写手册的人语文不及格。。。要么是写的有错误,要么是重要的事情没有说全。。。(不看代码,只看手册必走弯路)

在这里插入图片描述

嵌入式是什么?

在我和我的没有经验的队友沟通的时候,我惊讶于我默认是常识的东西,他是不知道的。仔细想来,经验或许就是这种东西吧。所以我打算先用最简单的话讲一下基本的嵌入式常识:

嵌入式,或者说单片机,就是通过编程,去操作针脚的电压高低,进而实现控制功能的一种设备。

比如,这里的机械臂和STM开发板(STM是常见的单片机),是我们编写了程序之后,把程序迁移到STM芯片之中,上电之后,STM控制针脚(称为GPIO)的电平,把控制信号传递给组成机械臂的电机,进而操作机械臂的运行。

这里说的开发板,不光指的是芯片本身,开发板上有一些板载的控制设备,比如按键之类的,我们可以通过这些额外的外设来发送人类的控制命令。

这么多板子是干什么?

嗯,这么说来。单片机和我们的PC机几乎没什么差别啊,都是控制设备,唯一区别可能是性能比较有限。嗯,这也是发这么多板子的原因。因为单片机性能太弱,所以我们一般是一块单片机只能执行一个任务。但是我们的目标是什么?(详情请自行阅读)要实现语音控制机械臂

STM已经承担了控制机械臂的任务,那语音识别只能交给别的单片机了。所以要发这么多块板子。我们的语音识别模块,就是那块黑色小板子+绿色开发板(ARDUINO UNO R3 )完成的。

不过,就像人类的小组分工一样,我们每个成员职能不同,但是想要完成一个任务,不得不有“沟通”,这些板子之间也要有沟通(信息传输)。这里的信息传输方式,其实也是通过在一个针脚上的电压高高低低来传递的,这里我们使用的传输方式名叫“串口(UART)”。这个在后面是需要重点讲解的内容。这里知道个大概就行了。

UNO 语音识别模块

点个灯吧

事先声明,你的机械臂如果是正常的话,是不需要往STM开发板上烧录代码和程序的,需要修改的只有UNO开发板上的程序。

对于经验不足的同学,我建议你还是先跟着我一起写程序,烧录,点个灯。然后再进行下面的内容,可以少走弯路。(好多人就是因为不会烧录导致的问题)我说白了,如果你连点灯都没成功,后面烧录语音识别就更别提了

UNO上的针脚

还是那个观点

嵌入式,或者说单片机,就是通过编程,去操作针脚的电压高低,进而实现控制功能的一种设备。

UNO的GPIO表如下:
在这里插入图片描述

不得不说,这个arduino 的板子做工真不错。怪不得是多少电子爱好者的入门板子。(虽然IO少了一点)
可以看到,他有个板载LED(右 LED BUTLTIN  PB 5 也是D13)可以控制。
下面让我们控制这个板载LED作为开始

这个地方有点奇怪,一个针脚怎么有两个名字? 嗯,这一点在下面(拓展解说)讲了。

开发环境Arduino 下载

在我整理的资料包里,第一个 点灯文件夹中 开发环境下载中 Arduino-1.8.3-windows.exe就是了。双击运行,即可完成安装。

coding !!!

我们的目标是:控制 Arduino 板载 LED(通常连接在 D13 引脚)实现 2 秒亮、2 秒灭的闪烁


OK,先打开我们已经下载好的arduino 客户端。注意先选择开发板型号。
在这里插入图片描述
输入我们的程序

void setup() {// 初始化板载LED对应的引脚(D13)为输出模式pinMode(13, OUTPUT);
}void loop() {// 点亮LED(将D13引脚置为高电平)digitalWrite(13, HIGH);// 延时2000毫秒(2秒)delay(2000);// 熄灭LED(将D13引脚置为低电平)digitalWrite(13, LOW);// 延时2000毫秒(2秒)delay(2000);
}

然后点击编译按钮。显示编译成功再进行下一步。
在这里插入图片描述
烧录程序

注意,使用USB接口连接到电脑,要先去设备管理器看自己的端口,再选完端口之后再烧录。否则一定失败!
在这里插入图片描述
注意,这一步需要先查看我们的设备管理器,找到UNO所在的端口,然后选择正确的端口,最后下载。出现这样的消息

最好观察LED ,确实是2s 暗 2s 亮
在这里插入图片描述

拓展解说:

为什么uno r3 的PIN的定义有两排?比如D13 和 PB5 并列?一个PIN能对应两个口吗

  1. 两类命名的本质
  • D13(Arduino 数字引脚):是面向用户的逻辑引脚命名,方便开发者快速识别和使用引脚,属于 “应用层” 的标识。
  • PB5(AVR 单片机端口引脚):是单片机(如 ATmega328P)的硬件端口命名,属于 “硬件层” 的原生标识,反映了引脚在单片机内部的寄存器映射关系。
  1. 为什么一个引脚有两个名称?

这两个名称指向同一个物理引脚,只是命名体系不同

  • Arduino 的 “Dxx” 命名是为了简化用户操作,让开发者无需深入单片机硬件细节,就能快速调用引脚。
  • 单片机的 “Px.x” 命名是硬件原生标识,当需要进行底层硬件操作(如直接配置单片机寄存器、实现更复杂的时序控制)时,会用到这种命名。

以 D13 和 PB5 为例:

  • 在 Arduino 程序中,你可以用digitalWrite(13, HIGH)来控制这个引脚,这里的 “13” 就是用户层面的 D13。
  • 若需要直接操作单片机寄存器(比如配置端口为输出),则会用到PORTB |= (1<<5)(其中 PB5 对应 PORTB 的第 5 位),这是硬件层面的 PB5。

语音识别,启动

嗯,恭喜你。已经完成了点灯。下面我们进入正题:先实现这个语音识别模块的代码。这个部分的基础代码,老师已经给出来了。

烧录新程序

资料在第二个包:语音识别代码中 那个ino 文件。 双击选择使用ARDUINO打开即可。按照上一个流程烧录即可。

/****************************************************Company:   幻尔科技作者:深圳市幻尔科技有限公司我们的店铺:lobot-zone.taobao.com
*****************************************************传感器:Hiwonder系列 语音识别模块通信方式:iic返回:数字量
*****************************************************/
#include <Wire.h>
/*只能识别汉字,将要识别的汉字转换成拼音字母,每个汉字之间空格隔开,比如:幻尔科技 --> huan er ke ji最多添加50个词条,每个词条最长为79个字符,每个词条最多10个汉字每个词条都对应一个识别号(1~255随意设置)不同的语音词条可以对应同一个识别号,比如“幻尔科技”和“幻尔”都可以将识别号设置为同一个值模块上的STA状态灯:亮起表示正在识别语音,灭掉表示不会识别语音,当识别到语音时状态灯会变暗,或闪烁,等待读取后会恢复当前的状态指示
*/
#define I2C_ADDR		0x79#define ASR_RESULT_ADDR           100
//识别结果存放处,通过不断读取此地址的值判断是否识别到语音,不同的值对应不同的语音,
#define ASR_WORDS_ERASE_ADDR      101//擦除所有词条
#define ASR_MODE_ADDR             102
//识别模式设置,值范围1~3
//1:循环识别模式。状态灯常亮(默认模式)
//2:口令模式,以第一个词条为口令。状态灯常灭,当识别到口令词时常亮,等待识别到新的语音,并且读取识别结果后即灭掉
//3:按键模式,按下开始识别,不按不识别。支持掉电保存。状态灯随按键按下而亮起,不按不亮
#define ASR_ADD_WORDS_ADDR        160//词条添加的地址,支持掉电保存bool WireWriteByte(uint8_t val)
{Wire.beginTransmission(I2C_ADDR);Wire.write(val);if ( Wire.endTransmission() != 0 ) {return false;}return true;
}bool WireWriteDataArray(  uint8_t reg, uint8_t *val, unsigned int len)
{unsigned int i;Wire.beginTransmission(I2C_ADDR);Wire.write(reg);for (i = 0; i < len; i++) {Wire.write(val[i]);}if ( Wire.endTransmission() != 0 ) {return false;}return true;
}int WireReadDataArray(   uint8_t reg, uint8_t *val, unsigned int len)
{unsigned char i = 0;/* Indicate which register we want to read from */if (!WireWriteByte(reg)) {return -1;}Wire.requestFrom(I2C_ADDR, len);while (Wire.available()) {if (i >= len) {return -1;}val[i] = Wire.read();i++;}/* Read block data */return i;
}/*添加词条函数,idNum:词条对应的识别号,1~255随意设置。识别到该号码对应的词条语音时,会将识别号存放到ASR_RESULT_ADDR处,等待主机读取,读取后清0words:要识别汉字词条的拼音,汉字之间用空格隔开执行该函数,词条是自动往后排队添加的。
*/
bool ASRAddWords(unsigned char idNum, unsigned char *words)
{Wire.beginTransmission(I2C_ADDR);Wire.write(ASR_ADD_WORDS_ADDR);Wire.write(idNum);Wire.write(words, strlen(words));if ( Wire.endTransmission() != 0 ) {delay(10);return false;}delay(10);return true;
}void setup()
{uint8_t ASRMode = 3;//1:循环识别模式    2:口令模式,以第一个词条为口令    3按键模式,按下开始识别Wire.begin();Serial.begin(9600);#if 1   //添加的词条和识别模式是可以掉电保存的,第一次设置完成后,可以将此段注释掉,即将1改为0,然后重新下载一次程序WireWriteDataArray(ASR_WORDS_ERASE_ADDR, NULL, 0);delay(60);//擦除需要一定的时间ASRAddWords(1, "kai shi");            //开始ASRAddWords(2, "ni hao");             //你好ASRAddWords(3, "huan er ke ji");      //幻尔科技ASRAddWords(3, "huan er");            //幻尔ASRAddWords(4, "shen zhen shi");      //深圳市if (WireWriteDataArray(ASR_MODE_ADDR, &ASRMode, 1))Serial.println("ASR Module Initialization complete");elseSerial.println("ASR Module Initialization fail");
#endifSerial.println("Start");
}void loop()
{unsigned char result;delay(1);WireReadDataArray(ASR_RESULT_ADDR, &result, 1);if (result){Serial.print("ASR result is:");Serial.println(result);//返回识别结果,即识别到的词条编号}
}

连线与使用

嗯,烧录完程序记得断电连线。

下面我们来看一下线怎么连:
在这里插入图片描述
旁边都有标记,考上西电的智力应该都正常,这里不多讲。(这里的SCL和SDA是这两块小板的通信方式,名为IIC)


使用方法也很简单,点开串口监视器。

我这里设置的是按键识别模式:点击按钮,说自己要识别的内容,说完之后他会识别出来,并返回编码的值。(此程序中,只有开始 你好… 这些简单词汇 )
在这里插入图片描述
代码比较直观,可以按照自己的需要修改。
嗯,做完这个之后,整个机械臂其实已经完成了30%了。嗯,做完这个之后,整个机械臂其实已经完成了30 \%了。 30%

机械臂

事先声明,你的机械臂如果是正常的话,是不需要往STM开发板上烧录代码和程序的,需要修改的只有UNO开发板上的程序。

上位机

嗯,还是上面那句话,机械臂的代码不用改,我们只要使用“上位机”程序通过串口,去操作和下载动作到机械臂中去就行了。上位机的程序在第三个包中,操作方法附录视频已经给出,非常简单,有手就行。我就不多讲了。

唯一要注意的,把机械臂放在开阔位置,周围不要有显示屏等易碎品。
这里主要想说的是:上位机实现了把动作组存储到机械臂系统的功能。这个简单的结论后面要用。这里主要想说的是:上位机实现了把动作组存储到机械臂系统的功能。 \\ 这个简单的结论后面要用。

串口操作

嗯,这个文档给的不是很好,我很多东西是通过读代码读出来的。如果对分析感兴趣的,可以读读比较吃操作的代码分析一节。

这一节我们来试试用PC跟STM串口通信,发送命令,控制机械臂。对了,请保证你按照上面的方法在100号动作中下载好了你的程序。

打开我给的资料包中的第四节,里面有一个串口软件。

这个MICRO USB 内部是一个CH340 实现和PC的通信
在这里插入图片描述
按照如图来配置:

注意勾选HEX发送,发送的神奇喵喵咒语是:

55550406640100

这个是串口命令效果:运行100号动作组 1次。 如果对分析过程感兴趣,或者想修改这个动作组编号的话,看下一节
在这里插入图片描述
恭喜你,已经完成了60%!!!恭喜你,已经完成了60\% !!!60%!!!

* 比较吃操作的代码分析

嗯,这个小节讲的我的是怎么分析出来上面的神奇喵喵咒语的。不感兴趣,只想知道这个命令怎么编可以看最后的结论

打开第5个文件夹,进入KEIL。


主程序是:

#include "include.h"int main(void)
{uint8 ps_ok = 1;SystemInit(); 			 //系统时钟初始化为72M	  SYSCLK_FREQ_72MHzInitDelay(72);	     //延时初始化NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	//设置NVIC中断分组2:2位抢占优先级,2位响应优先级InitPWM();InitTimer2();//用于产生100us的定时中断InitUart1();//用于与PC端进行通信InitUart3();//外接模块的串口InitADC();InitLED();InitKey();InitBuzzer();ps_ok = InitPS2();//PS2游戏手柄接收器初始化InitFlash();InitMemory();InitBusServoCtrl();LED = LED_ON;/*BusServoCtrl(1,SERVO_MOVE_TIME_WRITE,500,1000);BusServoCtrl(2,SERVO_MOVE_TIME_WRITE,500,1000);BusServoCtrl(3,SERVO_MOVE_TIME_WRITE,500,1000);BusServoCtrl(4,SERVO_MOVE_TIME_WRITE,500,1000);BusServoCtrl(5,SERVO_MOVE_TIME_WRITE,500,1000);BusServoCtrl(6,SERVO_MOVE_TIME_WRITE,500,1000);*/while(1){TaskRun(ps_ok);}
}

最重要的是这两个函数

InitUart1();//用于与PC端进行通信
...
TaskRun(ps_ok);

帧协议分析

第一个函数是初始化我们的串口通信:选中点击F 12 进入详情

代码拆分

InitUart1
  • 功能:完成 USART1 的硬件初始化,包括 GPIO 配置、串口参数设置、中断使能。
  • 关键配置
    • 引脚:TX=PA9(复用推挽输出),RX=PA10(上拉输入)。
    • 串口参数:波特率 9600、8 位数据位、1 位停止位、无校验、无硬件流控,同时使能收发模式。
    • 中断配置:使能接收非空中断(USART_IT_RXNE),并配置 NVIC 中断优先级(抢占优先级 1,子优先级 0),确保接收数据时能触发中断处理。
void InitUart1(void)
{NVIC_InitTypeDef NVIC_InitStructure;GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;
//	NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE);//USART1_TX   PA.9GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_Init(GPIOA, &GPIO_InitStructure);//USART1_RX	  PA.10GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_Init(GPIOA, &GPIO_InitStructure);//USART 初始化设置USART_InitStructure.USART_BaudRate = 9600;//一般设置为9600;USART_InitStructure.USART_WordLength = USART_WordLength_8b;USART_InitStructure.USART_StopBits = USART_StopBits_1;USART_InitStructure.USART_Parity = USART_Parity_No;USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;USART_Init(USART1, &USART_InitStructure);USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断USART_Cmd(USART1, ENABLE);                    //使能串口NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;		//NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能NVIC_Init(&NVIC_InitStructure);	//根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器USART1
}
发送TX代码
  • Uart1SendData:发送单个字节。通过查询 USART1 的状态寄存器(SR)的TXE位(发送缓冲区空),等待发送完成后再写入数据。
  • UART1SendDataPacket:发送数据包(多个字节)。循环调用单字节发送函数,依次发送count个字节,确保数据包完整发送。
  • McuToPCSendData:构造 MCU 向 PC 发送的标准化数据包。帧结构为:0x55 0x55 [长度] [命令] [参数1] [参数2],再调用数据包发送函数发送。
void Uart1SendData(BYTE dat)
{while((USART1->SR&0X40)==0);//循环发送,直到发送完毕USART1->DR = (u8) dat;while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
}void UART1SendDataPacket(uint8 dat[],uint8 count)
{uint32 i;for(i = 0; i < count; i++){
//		USART1_TransmitData(tx[i]);while((USART1->SR&0X40)==0);//循环发送,直到发送完毕USART1->DR = dat[i];while((USART1->SR&0X40)==0);//循环发送,直到发送完毕}
}void McuToPCSendData(uint8 cmd,uint8 prm1,uint8 prm2)
{uint8 dat[8];uint8 datlLen = 2;switch(cmd){//		case CMD_ACTION_DOWNLOAD:
//			datlLen = 2;
//			break;default:datlLen = 2;break;}dat[0] = 0x55;dat[1] = 0x55;dat[2] = datlLen;dat[3] = cmd;dat[4] = prm1;dat[5] = prm2;UART1SendDataPacket(dat,datlLen + 2);
}
接收RX代码
  • 功能:处理串口接收中断,解析符合协议的帧数据,完成后标记接收完成。
  • 帧协议解析逻辑
    • 帧头:以连续两个0x55作为帧起始标志(通过startCodeSum计数检测)。
    • 帧长度:帧头后第 3 个字节(Uart1RxBuffer[2])为数据长度(messageLengthSum)。
    • 数据接收:从帧头开始累计接收字节数(messageLength),当接收长度达到messageLengthSum + 2(帧头 2 字节 + 长度 1 字节 + 数据部分)时,标记接收完成(fUartRxComplete = TRUE),并将数据从临时缓冲区(Uart1RxBuffer)复制到处理缓冲区(UartRxBuffer)。
  • 异常处理:若接收过程中未检测到连续0x55,则重置帧解析状态(fFrameStart = FALSE)。
void USART1_IRQHandler(void)
{uint8 i;uint8 rxBuf; // 读到的消息static uint8 startCodeSum = 0;static bool fFrameStart = FALSE;static uint8 messageLength = 0;static uint8 messageLengthSum = 2;//----------------------------------------------------// 帧头:两个连续的0x55 开启(在fFrameStart= false 的时候触发,否则跳过)if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){rxBuf = USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数据if(!fFrameStart){if(rxBuf == 0x55){startCodeSum++;if(startCodeSum == 2){startCodeSum = 0;fFrameStart = TRUE;messageLength = 1;}}else{//不是两个连续的0x55 则重置标记fFrameStart = FALSE;messageLength = 0;startCodeSum = 0;}}//------------------------------------------------// fFrameStart = true , 标志进入消息if(fFrameStart){Uart1RxBuffer[messageLength] = rxBuf;//把消息(rxBuf)写入缓冲区if(messageLength == 2){// 这个messageLengthSum 起到什么作用?messageLengthSum = Uart1RxBuffer[messageLength];if(messageLengthSum < 2)// || messageLengthSum > 30{messageLengthSum = 2;fFrameStart = FALSE;}}messageLength++;//增长messageLength  写入下一个位置if(messageLength == messageLengthSum + 2) {if(fUartRxComplete == FALSE){fUartRxComplete = TRUE;for(i = 0;i < messageLength;i++){UartRxBuffer[i] = Uart1RxBuffer[i];}}fFrameStart = FALSE;}}}}
TaskPCMsgHandle命令处理模块
  • 功能:在主循环中轮询接收完成标志,解析 PC 发送的命令并执行对应操作。
  • 核心逻辑
    • 通过UartRxOK函数检查是否有完整帧接收(返回fUartRxComplete状态并清零)。
    • 解析命令:从接收缓冲区第 4 个字节(UartRxBuffer[3])获取命令字(cmd)。
    • 命令执行:根据不同命令执行对应操作,例如:
      • CMD_MULT_SERVO_MOVE:控制多个舵机移动(解析舵机数量、时间、位置参数)。
      • CMD_FULL_ACTION_RUN:运行指定动作组(解析动作组编号和运行次数)。
      • CMD_FULL_ACTION_ERASE:擦除所有动作组数据。
      • CMD_ACTION_DOWNLOAD:保存动作数据到 Flash(调用SaveAct)。
static bool UartRxOK(void)
{if(fUartRxComplete){fUartRxComplete = FALSE;return TRUE;}else{return FALSE;}
}void TaskPCMsgHandle(void)
{uint16 i;uint8 cmd;uint8 id;uint8 servoCount;uint16 time;uint16 pos;uint16 times;uint8 fullActNum;if(UartRxOK()){LED = !LED;cmd = UartRxBuffer[3];switch(cmd){case CMD_MULT_SERVO_MOVE:servoCount = UartRxBuffer[4];time = UartRxBuffer[5] + (UartRxBuffer[6]<<8);for(i = 0; i < servoCount; i++){id =  UartRxBuffer[7 + i * 3];pos = UartRxBuffer[8 + i * 3] + (UartRxBuffer[9 + i * 3]<<8);ServoSetPluseAndTime(id,pos,time);BusServoCtrl(id,SERVO_MOVE_TIME_WRITE,pos,time);}				break;case CMD_FULL_ACTION_RUN:fullActNum = UartRxBuffer[4];//动作组编号times = UartRxBuffer[5] + (UartRxBuffer[6]<<8);//运行次数McuToPCSendData(CMD_FULL_ACTION_RUN, 0, 0);FullActRun(fullActNum,times);break;case CMD_FULL_ACTION_STOP:FullActStop();break;case CMD_FULL_ACTION_ERASE:FlashEraseAll();McuToPCSendData(CMD_FULL_ACTION_ERASE,0,0);break;case CMD_ACTION_DOWNLOAD:SaveAct(UartRxBuffer[4],UartRxBuffer[5],UartRxBuffer[6],UartRxBuffer + 7);McuToPCSendData(CMD_ACTION_DOWNLOAD,0,0);break;}}
}
Flash 存储辅助模块(存储相关函数)
  • SaveAct:将动作数据写入 Flash。若为新动作组(frameIndex=0),先擦除对应扇区;写入当前帧数据;当最后一帧写入完成后,更新动作组帧计数并保存到 Flash。
  • FlashEraseAll:擦除所有动作组(将所有动作组的帧计数设为 0,写入 Flash)。
  • InitMemory:初始化存储器。检查 Flash 中是否有标志 “LOBOT”,若未检测到(新 Flash),则初始化标志并擦除所有动作组数据。
void SaveAct(uint8 fullActNum,uint8 frameIndexSum,uint8 frameIndex,uint8* pBuffer)
{uint8 i;if(frameIndex == 0)//下载之前先把这个动作组擦除{//一个动作组占16k大小,擦除一个扇区是4k,所以要擦4次for(i = 0;i < 4;i++)//ACT_SUB_FRAME_SIZE/4096 = 4{FlashEraseSector((MEM_ACT_FULL_BASE) + (fullActNum * ACT_FULL_SIZE) + (i * 4096));}}FlashWrite((MEM_ACT_FULL_BASE) + (fullActNum * ACT_FULL_SIZE) + (frameIndex * ACT_SUB_FRAME_SIZE),ACT_SUB_FRAME_SIZE,pBuffer);if((frameIndex + 1) ==  frameIndexSum){FlashRead(MEM_FRAME_INDEX_SUM_BASE,256,frameIndexSumSum);frameIndexSumSum[fullActNum] = frameIndexSum;FlashEraseSector(MEM_FRAME_INDEX_SUM_BASE);FlashWrite(MEM_FRAME_INDEX_SUM_BASE,256,frameIndexSumSum);}
}void FlashEraseAll(void)
{//将所有255个动作组的动作数设置为0,即代表将所有动作组擦除uint16 i;for(i = 0;i <= 255;i++){frameIndexSumSum[i] = 0;}FlashEraseSector(MEM_FRAME_INDEX_SUM_BASE);FlashWrite(MEM_FRAME_INDEX_SUM_BASE,256,frameIndexSumSum);
}void InitMemory(void)
{uint8 i;uint8 logo[] = "LOBOT";uint8 datatemp[8];FlashRead(MEM_LOBOT_LOGO_BASE,5,datatemp);for(i = 0; i < 5; i++){if(logo[i] != datatemp[i]){LED = LED_ON;//如果发现不相等的,则说明是新FLASH,需要初始化FlashEraseSector(MEM_LOBOT_LOGO_BASE);FlashWrite(MEM_LOBOT_LOGO_BASE,5,logo);FlashEraseAll();break;}}}

帧协议

他这个代码,我只能说写的比较烂。不过我倒是反向读出来一些东西,首先是“帧结构”

字节位置(对应 Uart1RxBuffer 索引)内容含义备注
0无内容这是程序的一个小瑕疵
1帧头第 2 个 0x55帧头是 2 个 0x55,第一个 0x55 在触发帧开始时没存?不,看代码:messageLength 从 1 开始计数,当帧头检测完成(2 个 0x55),messageLength=1,此时第一个存的是第 2 个 0x55(因为第一个 0x55 在检测帧头时没写入缓冲区,第二个 0x55 才是缓冲区第一个数据)
2长度字段这就是 messageLengthSum 的来源!
3 ~ (2 + messageLengthSum)有效数据包括命令、参数(比如舵机 ID、位置等)

为了清晰展示状态变化,我们以一个典型的完整帧传输为例:假设 PC 发送的帧数据为 0x55(帧头1)→ 0x55(帧头2)→ 0x03(长度)→ 0x01(数据1)→ 0x02(数据2)→ 0x03(数据3)(共 6 个字节,符合协议的完整帧)。

每次中断触发(接收 1 个字节)时,各状态量的变化如下表:

接收顺序rxBuf(当前接收的字节)startCodeSum(帧头计数)fFrameStart(帧开始标志)messageLength(已接收字节数)messageLengthSum(帧长度)备注(当前操作)
10x55(第一个帧头)0 → 1FALSE02(初始值)检测到第一个 0x55,帧头计数 + 1
20x55(第二个帧头)1 → 0(重置)FALSE → TRUE0 → 12(初始值)检测到第二个 0x55,帧开始标志激活,接收计数从 1 开始
30x03(长度字段)0TRUE1 → 22(未更新)将 0x03 存入 Uart1RxBuffer [1],接收计数 + 1
40x01(第一个数据)0TRUE2 → 32 → 0x03(更新)此时 messageLength=2,读取长度字段(0x03);将 0x01 存入 Uart1RxBuffer [2],计数 + 1
50x02(第二个数据)0TRUE3 → 40x03将 0x02 存入 Uart1RxBuffer [3],计数 + 1
60x03(第三个数据)0TRUE → FALSE4 → 50x03将 0x03 存入 Uart1RxBuffer [4],计数 + 1;此时 messageLength=5(0x03+2),帧接收完成,标志重置
  1. 前两次接收是 “帧头同步”:通过startCodeSum计数连续的 0x55,直到累计 2 个才激活帧开始。
  2. 第三次接收的是 “长度字段”,但此时messageLength=2,要到第四次接收时才会读取该字段并更新messageLengthSum
  3. 最后一次接收时,messageLength达到 “长度 + 2”(0x03+2=5),触发帧完成逻辑(复制缓冲区、重置标志)。

整个过程通过静态变量的状态流转,实现了从 “等待帧头” 到 “接收数据” 再到 “帧完成” 的完整解析。

TaskRun循环

机械臂循环跑的内容是TaskRun

while(1){TaskRun(ps_ok);}

跳转到这个函数内部,哇,好长。但是实际上,后面的400 行内容在if (ps2_ok == 0)这个条件里。而老师没给PS手柄模块。所以这400 行代码不用看。ψ(`∇´)ψ 那代码只剩下了

void TaskRun(u8 ps2_ok)
{static bool Ps2State = FALSE;static uint8 mode = 0;uint16 ly, rx,ry;uint8 PS2KeyValue;static uint8 keycount = 0;//-------------------------------------TaskTimeHandle();TaskPCMsgHandle();TaskBLEMsgHandle();TaskRobotRun();//-------------------------------------// 下面是KEY相关的按键检测// 这里是按下KEY1(开发板板载按钮,执行100 号动作1 次)if(KEY == 0){DelayMs(60);{if(KEY == 0){keycount++;}else{if (keycount > 20){keycount = 0;FullActRun(100,0);return;}else{keycount = 0;LED = ~LED;FullActRun(100,1);	}}}}//。。。跳过PS2手柄代码
}

核心运行逻辑

整个函数每次循环都会按以下步骤执行,和 PS2 无关的核心逻辑都在前面:

执行 4 个基础核心任务(每次循环必跑)

这 4 个函数是整个系统的 “骨架”,不管有没有 PS2,每次都会执行,负责定时、消息处理、机器人主体运行:

  • TaskTimeHandle():定时任务处理(处理定时器相关逻辑,比如舵机运动计时、动作组进度计时等)。
  • TaskPCMsgHandle():PC 串口消息处理(就是之前分析的 “接收 PC 命令→控制舵机 / 动作组” 的逻辑)。
  • TaskBLEMsgHandle():BLE 蓝牙消息处理(和 PC 消息类似,只是通过蓝牙接收控制命令)。
  • TaskRobotRun():机器人主体运行逻辑(推测是执行动作组、舵机运动等实际控制逻辑)。

结论

如果我们想发送“执行100号动作1次”的命令:

帧协议格式为:[帧头][长度][命令][动作组编号][运行次数低8位][运行次数高8位],具体字节如下:

字节位置含义数值(十六进制)说明
0帧头 10x55固定帧头(第一个 0x55)
1帧头 20x55固定帧头(第二个 0x55)
2长度字段0x04表示 “命令 + 参数” 的总字节数(1+1+2=4)
3命令(cmd)0x06对应CMD_FULL_ACTION_RUN
4动作组编号0x64100 的十六进制(十进制 100)
5运行次数低 8 位0x01运行次数 = 1(低 8 位为 1)
6运行次数高 8 位0x00运行次数 = 1(高 8 位为 0,1=0x0001)

按顺序发送以下 7 个字节即可:0x55, 0x55, 0x04, 0x06, 0x64, 0x01, 0x00

余下的修改,自己想怎么改怎么改
读懂这个结论,你已经完成了75%!!!读懂这个结论,你已经完成了75 \% !!! 75%

收尾

上面的工作都是依赖电脑的,但是实际上验收的时候必须脱机。不过我们已经算完成了。

UNO 串口

所以,嗯,如果上面都验证完了,那我们既然可以PC发串口消息,机械臂接收,怎么不能UNO发串口,机械臂接收呢?

那UNO代码可以这么写:

我们把口令映射到数字,写成宏LEFT && RIGHT

嗯,下面的代码仅为示意,

// 定义要发送的十六进制字节数组(对应55550406010100)
uint8_t cmdr[] = {0x55, 0x55, 0x04, 0x06, 0x01, 0x01, 0x00};uint8_t cmdl[] = {0x55, 0x55, 0x04, 0x06, 0x64, 0x01, 0x00};// 数组长度(共7个字节)
uint8_t cmdLen = sizeof(cmd) / sizeof(cmd[0]);void loop()
{unsigned char result;delay(1);WireReadDataArray(ASR_RESULT_ADDR, &result, 1);if (result){Serial.print("ASR result is:");Serial.println(result);//返回识别结果,即识别到的词条编号if(result == LEFT){// 发送字节数组(原始字节形式,即HEX格式)Serial.write(cmdl, cmdLen);// 发送一次后延时,避免重复发送(根据需求调整)delay(2000);}if(result == RIGHT){// 发送字节数组(原始字节形式,即HEX格式)Serial.write(cmdr, cmdLen);// 发送一次后延时,避免重复发送(根据需求调整)delay(2000);}}}

连线

在这里插入图片描述
注意,TX RX需要交错连接。
这里蓝图已经给出,已经把90%的东西都交给你了,剩下自己调试的,应该能行吧?这里蓝图已经给出,已经把90\%的东西都交给你了,剩下自己调试的,应该能行吧? 90%西

最后的话

请按照自己的理解自行修改(毕竟电鼠可不想别人的作业和自己一样)我觉得自己已经讲的够清楚了,这么多通俗易懂的话,这么多图片,我还把那凌乱的文档整理了出来…不理解只能自己烧高香了。

一下午码了2万5000字可是累死我了。(;´д`)ゞ

实在不理解… 也可以线下找我提供指导,但是毕竟我非常懒,只想呆在自己的鼠窝里, 只能说求人不如求己了。

不过有小米赚那另当别论ヾ(•ω•`)o (知识付费,bro)(啮齿类动物狂笑)
在这里插入图片描述
如果对你有帮助的话,还请点赞,收藏,关注。这会让我开心一整天~ ୧⍢⃝୨

http://www.dtcms.com/a/499319.html

相关文章:

  • 数据结构——队列的链式存储结构
  • 媒体135网站口碑好的宜昌网站建设
  • 湖南省建设银行网站官网深圳龙华网站建设公司
  • 网站后台管理系统源码网站空间文件夹
  • 元宇宙与公共服务的深度融合:重构民生服务的效率与温度
  • 深入解析十字链表:从理论到实践的全面指南
  • 红色页面网站护肤品网站建设的摘要
  • GB28181视频服务wvp部署(一)
  • 吴忠住房和城乡建设局网站小学生编程网课前十名
  • 浅谈 OpenAPI Schema—— 接口契约的标准语言
  • TSDF 体素模型与光线投射
  • 【学习笔记】利用meshlab进行曲面的质量检查
  • S2--单链表
  • jdk.random 包详解
  • 如何做网站接口关于电子商务网站建设的现状
  • 网站栏目设计内容谷歌在线浏览器入口
  • 聊聊 Unity(小白专享、C# 小程序 之 自动更新)
  • 截取网站流量dede购物网站
  • 某Boss直聘数据获取
  • Spring Boot 3零基础教程,WEB 开发 默认欢迎页 笔记28
  • Redis极简入门 整合springboot
  • 漫蛙漫画官网入口 - 免费漫画在线看|防走失页入口
  • MySQL中的约束详解
  • 服务流程企业网站东莞市建设安监监督网站
  • leetcode 206. 反转链表 python
  • 【C语言】自定义类型(附源码与图片分析)
  • 用户头像文件存储功能是如何实现的?
  • 网站设计大概在什么价位渠道销售
  • C++竞赛递推算法-斐波那契数列常见题型与例题详解
  • 单元测试-例子