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能对应两个口吗
- 两类命名的本质
- D13(Arduino 数字引脚) :是面向用户的逻辑引脚命名,方便开发者快速识别和使用引脚,属于 "应用层" 的标识。
- PB5(AVR 单片机端口引脚) :是单片机(如 ATmega328P)的硬件端口命名,属于 "硬件层" 的原生标识,反映了引脚在单片机内部的寄存器映射关系。
- 为什么一个引脚有两个名称?
这两个名称指向同一个物理引脚 ,只是命名体系不同:
- 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),帧接收完成,标志重置 |
- 前两次接收是 "帧头同步":通过
startCodeSum
计数连续的 0x55,直到累计 2 个才激活帧开始。 - 第三次接收的是 "长度字段",但此时
messageLength=2
,要到第四次接收时才会读取该字段并更新messageLengthSum
。 - 最后一次接收时,
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)(啮齿类动物狂笑)
如果对你有帮助的话,还请点赞,收藏,关注。这会让我开心一整天~ ୧⍢⃝୨