西电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 客户端。注意先选择开发板型号。

输入我们的程序

c 复制代码
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打开即可。按照上一个流程烧录即可。

C 复制代码
/****************************************************
  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处,等待主机读取,读取后清0
   words:要识别汉字词条的拼音,汉字之间用空格隔开
   执行该函数,词条是自动往后排队添加的。
*/
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");
  else
    Serial.println("ASR Module Initialization fail");
#endif

  Serial.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发送,发送的神奇喵喵咒语是:

CMD 复制代码
55550406640100

这个是串口命令效果:运行100号动作组 1次。 如果对分析过程感兴趣,或者想修改这个动作组编号的话,看下一节

恭喜你,已经完成了60%!!!恭喜你,已经完成了60\% !!!恭喜你,已经完成了60%!!!

* 比较吃操作的代码分析

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

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


主程序是:

C 复制代码
#include "include.h"



int main(void)
{
	uint8 ps_ok = 1;
	SystemInit(); 			 //系统时钟初始化为72M	  SYSCLK_FREQ_72MHz
	InitDelay(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),确保接收数据时能触发中断处理。
C 复制代码
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.9
	GPIO_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.10
	GPIO_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],再调用数据包发送函数发送。
C 复制代码
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)。
C 复制代码
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)。
C 复制代码
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),则初始化标志并擦除所有动作组数据。
C 复制代码
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(帧长度) 备注(当前操作)
1 0x55(第一个帧头) 0 → 1 FALSE 0 2(初始值) 检测到第一个 0x55,帧头计数 + 1
2 0x55(第二个帧头) 1 → 0(重置) FALSE → TRUE 0 → 1 2(初始值) 检测到第二个 0x55,帧开始标志激活,接收计数从 1 开始
3 0x03(长度字段) 0 TRUE 1 → 2 2(未更新) 将 0x03 存入 Uart1RxBuffer [1],接收计数 + 1
4 0x01(第一个数据) 0 TRUE 2 → 3 2 → 0x03(更新) 此时 messageLength=2,读取长度字段(0x03);将 0x01 存入 Uart1RxBuffer [2],计数 + 1
5 0x02(第二个数据) 0 TRUE 3 → 4 0x03 将 0x02 存入 Uart1RxBuffer [3],计数 + 1
6 0x03(第三个数据) 0 TRUE → FALSE 4 → 5 0x03 将 0x03 存入 Uart1RxBuffer [4],计数 + 1;此时 messageLength=5(0x03+2),帧接收完成,标志重置
  1. 前两次接收是 "帧头同步":通过startCodeSum计数连续的 0x55,直到累计 2 个才激活帧开始。
  2. 第三次接收的是 "长度字段",但此时messageLength=2,要到第四次接收时才会读取该字段并更新messageLengthSum
  3. 最后一次接收时,messageLength达到 "长度 + 2"(0x03+2=5),触发帧完成逻辑(复制缓冲区、重置标志)。

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

TaskRun循环

机械臂循环跑的内容是TaskRun

C 复制代码
while(1)
	{
		TaskRun(ps_ok);
	}

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

c 复制代码
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 帧头 1 0x55 固定帧头(第一个 0x55)
1 帧头 2 0x55 固定帧头(第二个 0x55)
2 长度字段 0x04 表示 "命令 + 参数" 的总字节数(1+1+2=4)
3 命令(cmd) 0x06 对应CMD_FULL_ACTION_RUN
4 动作组编号 0x64 100 的十六进制(十进制 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

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

c 复制代码
// 定义要发送的十六进制字节数组(对应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)(啮齿类动物狂笑)

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

相关推荐
START_GAME2 天前
语音合成系统---IndexTTS2:环境配置与实战
人工智能·语音识别
疯笔码良2 天前
【Flutter】flutter安装并在Xcode上应用
flutter·macos·xcode
lichong9513 天前
【Xcode】Macos p12 证书过期时间查看
前端·ide·macos·证书·xcode·大前端·大前端++
_阿南_3 天前
flutter在Xcode26打包的iOS26上全屏支持右滑的问题
flutter·ios·xcode
逐星ing3 天前
【AIGC】语音识别ASR:火山引擎大模型技术实践
aigc·语音识别·火山引擎
文火冰糖的硅基工坊5 天前
[嵌入式系统-107]:语音识别的信号处理流程和软硬件职责
人工智能·语音识别·信号处理
一品威客网5 天前
语音控制 APP 开发:唤醒率 99% 的实现
人工智能·语音识别
星野云联AIoT技术洞察5 天前
2025年语音识别(ASR)与语音合成(TTS)技术趋势分析对比
whisper·语音识别·模型部署·tts·asr·嵌入式ai·naturalspeech3
驰羽5 天前
[GO]Go语言泛型详解
开发语言·golang·xcode