使用K210和STM32打造桌面小机器人
一直想尝试自己动手diy一个像Loona、Vector、Comoz等一样的宠物机器人,毕竟在网上见识过这类产品的人都知道它们的功能是非常神奇的存在,宠物机器人内置了丰富人机交互功能,会有情绪波动,会对人撒娇,会调皮捣蛋搞破坏,让人感觉仿佛这些真的是有生命的小精灵。然而这些产品的售价也不菲,而且网上许多购买过该产品的用户后来也都转二手卖出了,原因之一就是虽然这些宠物机器人有着像真宠物一样的行为,可实际时间久了就会发现其实它的功能来来回回就那几样,这时过了新鲜感的用户逐渐意识到机器人的一个一个表现其实不过是一串又一串预先配置的冷冰冰的代码罢了,因而自然也不会再被其所吸引。由于目前技术所限,宠物机器人还不足以真正做到“有生命”,这就是目前这一类产品的缺陷所在。
我想自己做一个,其中一方面就是为了能自由开发和更新升级机器的功能,另一方面也是想尝试用自己的想法去仿一个简单的宠物行为,探索其中的奥妙。当然,我目前是刚刚入手,想做到像上述的产品那样的功能还是十分困难的,在这一个项目中,我计划设定的功能都是比较简单的,目前想的是先实现简单的交互后再慢慢升级,探索开发复杂的功能。当然这个小机器人同时也是我给女朋友准备的礼物,女孩子当然是无法拒绝一只乖巧可爱富有活力的小“宠物”的。
硬件说明
以K210为主控,以迷你版的stm32为协处理器,K210与stm32采用串口通信。K210主要用于处理图像识别等大计算量的任务,stm32则是用于控制底层执行器,这次的执行器布置比较简单,只有两个舵机,一个负责让机器人抬头低头,一个负责左右转。
K210视觉识别模块
这里K210是在一款名为K210视觉识别模块的产品中,在某橙色软件可以搜到。该产品可以外形像一个小相机,自带了摄像头和LCD屏幕。在购买到手把玩一段时间后我发现恰好拆了亚克力板把摄像头扭向下,整个相机倒过来,恰好就可以做一个小机器人的头,摄像头做眼睛,屏幕做脸,而其中内置的K210芯片,能够跑轻量级视觉算法,恰巧能做机器人的大脑。

迷你stm32
这款stm32尺寸只有25.40*22.86mm大小,可以说是非常小巧玲珑了,非常适合用来做小体积的项目,它芯片的具体型号是stm32f103c8t6,其实就是最小系统板常用的那款型号。

stm32扩展板
这是自己绘制的用于连接的扩展板,结构非常简单,就两个舵机接口,以及串口通信接口,还有两个电源引脚线。

舵机
用的是MG90s,非常常见的小舵机。(某宝直接截的图,忽略图中的水印哈哈哈哈)

软件说明
程序主要是在K210官方例程的基础上改的,大体的布局就是K210跑视觉算法将相应的参数如识别出的人脸位置以及长度宽度,还有两个舵机的转角通过串口通信传输给stm32,而stm32则是将来自K210的转角值输入两个舵机中,从而是K210这颗大脑能指挥两个关节行动。
表情图像说明
表情图像用的是B站一位up主何时登陆何时还 开源的oled表情包,详情可点击下面链接。开源代码教程stm32的oled表情显示
不过我这边不像视频那样做,我直接把表情图像存入SD卡,再让K210从SD卡中读取图片并显示在LCD上,当然本质上都是一样的,就是逐帧显示照片形成动画。

stm32程序说明
stm32所用的程序也是在官方例程的基础上改的,比较简单,只是增加了两个舵机函数,让stm32不断地执行从K210读取到转角命令。以下是stm32的主程序代码,其余全部源码将在项目完成后公开。
#include "AllHeader.h"
char buff_com[50];
msg_k210 k210_msg;//收到k210信息结构体 p0-PB0 p1-PB1
int main()
{
SystemInit();
Servo_Init1(); //PB0 PB1
delay_init();
led_int();//PC13
USART2_init(115200);//PA2 PA3
LED = 0;
while(1)
{OLED_ShowNum(4, 1, k210_msg.class_n, 3);
if (k210_msg.class_n != 0)//例程号不为空
{
if(k210_msg.class_n == 5)//是人脸特征检测
{
Servo_SetAngle1(k210_msg.p1); //这是颈部舵机
Servo_SetAngle0(k210_msg.p0); //这是底座舵机
k210_msg.class_n = 0;//清除例程号
delay_ms(10);
}
}
}
}
K210程序说明
K210使用的编译软件是CanMV,以下代码的主要功能是让机器人醒着的时候盯着人看,当醒着的时间到4000秒(约67分钟)时,它会进入睡眠状态,当睡够3000秒即50分钟时会自然苏醒,当然睡眠状态下也可以通过触碰触摸屏将它唤醒。
import sensor, image, time, lcd
import touchscreen as ts
from maix import KPU
from modules import ybserial
import binascii
import gc
import random
serial = ybserial()
#有时运行失败应该是连接的问题,断开重新连一遍就可以了。
# 定义两种模式的图片列表
file_template = "/sd/EMO/{:04d}.bmp"
# 初始化一个空列表来存储文件路径
img_paths_mode1 = []
sleep =[]
# 定义一个循环,从0000开始,递增到0005
for i in range(8):
# 将当前的文件名添加到列表中
img_paths_mode1.append(file_template.format(i))
for i in range(10, 22, 1):
# 将当前的文件名添加到列表中
sleep.append(file_template.format(i))
img_paths_mode2 = [
"/sd/EMO/{:04d}.bmp".format(i) for i in range(0, 38, 5)
]
# 定义一个函数来选择显示模式
def choose_display_mode():
# 有80%的概率选择模式1,20%的概率选择模式2
s=random.random()
if s < 0.7:
print(s)
return img_paths_mode1
else:
print(s)
return img_paths_mode2
def str_int(data_str):
bb = binascii.hexlify(data_str)
bb = str(bb)[2:-1]
#print(bb)
#print(type(bb))
hex_1 = int(bb[0])*16
hex_2 = int(bb[1],16)
return hex_1+hex_2
def send_data(x,y,w,h,p0,p1,msg):
start = 0x24
end = 0x23
length = 5
class_num = 0x05 #例程编号
class_group = 0xBB #例程组
data_num = 0x00 #数据量
fenge = 0x2c #逗号
crc = 0 #校验位
data = [] #数据组
#x(小端模式)
low = x & 0xFF #低位
high = x >> 8& 0xFF #高位
data.append(low)
data.append(fenge) #增加","
data.append(high)
data.append(fenge) #增加","
#y(小端模式)
low = y & 0xFF #低位
high = y >> 8& 0xFF #高位
data.append(low)
data.append(fenge) #增加","
data.append(high)
data.append(fenge) #增加","
#w(小端模式)
low = w & 0xFF #低位
high = w >> 8& 0xFF #高位
data.append(low)
data.append(fenge) #增加","
data.append(high)
data.append(fenge) #增加","
#h(小端模式)
low = h & 0xFF #低位
high = h >> 8& 0xFF #高位
data.append(low)
data.append(fenge) #增加","
data.append(high)
data.append(fenge) #增加","
# p0
low = p0 & 0xFF # 低位
high = p0 >> 8 & 0xFF # 高位
data.append(low)
data.append(fenge) # 增加","
data.append(high)
data.append(fenge) # 增加","
# p1
low = p1 & 0xFF # 低位
high = p1 >> 8 & 0xFF # 高位
data.append(low)
data.append(fenge) # 增加","
data.append(high)
data.append(fenge) # 增加","
if msg !=None:
#msg
for i in range(len(msg)):
hec = str_int(msg[i])
data.append(hec)
data.append(fenge) #增加","
#print(data)
data_num = len(data)
length += len(data)
#print(length)
send_merr = [length,class_num,class_group,data_num]
for i in range(data_num):
send_merr.append(data[i])
#print(send_merr)
#不加上CRC位,进行CRC运算
for i in range(len(send_merr)):
crc +=send_merr[i]
crc = crc%256
send_merr.insert(0,start) #插入头部
send_merr.append(crc)
send_merr.append(end)
#print(send_merr)
global send_buf
send_buf = send_merr
last_time = time.time()
send_buf = []
x_ = 0
y_ = 0
w_ = 0
h_ = 0
p0_= 100
p0_0= 100
p1_= 57
p1_1= 57
k0=0.06
k1=0.04
X0=160
Y0=120
i = 0
sleepy =1
current_img_paths = choose_display_mode()
lcd.init()
ts.init()
status_last = ts.STATUS_IDLE
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time = 100)
clock = time.clock()
od_img = image.Image(size=(320,256))
anchor = (0.893, 1.463, 0.245, 0.389, 1.55, 2.58, 0.375, 0.594, 3.099, 5.038, 0.057, 0.090, 0.567, 0.904, 0.101, 0.160, 0.159, 0.255)
kpu = KPU()
kpu.load_kmodel("/sd/KPU/yolo_face_detect/yolo_face_detect.kmodel")
kpu.init_yolo2(anchor, anchor_num=9, img_w=320, img_h=240, net_w=320, net_h=256, layer_w=10, layer_h=8, threshold=0.7, nms_value=0.3, classes=1)
# 创建一个包含所有文件名的列表
img_paths = [
"/sd/EMO/{:04d}.bmp".format(i) for i in range(0,511,2)
]
# 现在 img_paths 列表包含了从 "0000.bmp" 到 "00511.bmp" 的所有文件名
print(img_paths)
while True: # time.sleep(0.5) # 等待0.2秒
gc.collect()
clock.tick()
img = sensor.snapshot()
a = od_img.draw_image(img, 0,0)
od_img.pix_to_ai()
kpu.run_with_output(od_img)
dect = kpu.regionlayer_yolo2()
fps = clock.fps()
#print("dect:",dect)
for l in dect :
a = img.draw_rectangle(l[0],l[1],l[2],l[3], color=(0, 255, 0))
x_ = l[0]
y_ = l[1]
w_ = l[2]
h_ = l[3]
p0_=round(p0_-(x_ +(h_/2)-X0)*k0+1)
p1_=round(p1_+(y_ +(w_/2)-Y0)*k0+1.5)
# print(p0_)
print(p0_)
print((x_ +(h_/2)-X0)*k0+1)
if sleepy==0: #清醒状态下将盯着人看
if p0_>180:
p0_0=180
p0_=p0_0
elif p0_<50:
p0_0=50
p0_=p0_0
else:
p0_0=p0_
if p1_>82:
p1_1=82
p1_=p1_1
elif p1_<57:
p1_1=57
p1_=p1_1
else:
p1_1=p1_
else: #睡眠状态下低头保持不动
p0_0=100
p1_1=82
# print(p0_)
print(p0_0)
send_data(x_,y_,w_,h_,p0_0,p1_1,None)
serial.send_bytearray(send_buf)
print(p0_0)
(status, j, k) = ts.read()
if status_last != status:
print(status, j, k)
status_last = status
if status == ts.STATUS_MOVE:
sleepy=0 #睡眠状态下如果用手轻微摩擦屏幕,那么会将它唤醒。
if sleepy==1:
if i <len(sleep): # 显示睡眠状态的表情
img_path = sleep[i]
img_read = image.Image(img_path)
img2 = img_read.resize(300, 150)
img2 = img2.rotation_corr(z_rotation=180)
i=i+1
lcd.display(img2)
else:
i=0
else:
if i <len(current_img_paths): # 显示正常状态的表情
img_path = current_img_paths[i]
img_read = image.Image(img_path)
img2 = img_read.resize(300, 150)
img2 = img2.rotation_corr(z_rotation=180)
i=i+1
lcd.display(img2)
else:
i=0
current_img_paths = choose_display_mode()
current_time = time.time()
# 检查是否到了下一秒的开始
if (current_time - last_time >= 4000)and(sleepy==0):#超过4000秒时自动进入睡眠状态
last_time = current_time
sleepy=1
print("I fall asleep", time.time())
elif (current_time - last_time >= 3000)and(sleepy==1):#睡够3000秒时自动苏醒
last_time = current_time
sleepy=0
print("I wake up", time.time())
time.sleep(0.01) # 短暂休眠以避免过度占用CPU
kpu.deinit()
效果展示
为了突出效果,视频中我把机器人的睡眠时间和清醒时间都调短了,大约每过五六秒就会睡着,视频里看起来像是它要睡觉但我一直骚扰它。
桌面机器人Raboty效果演示
结构说明
如图是用solidworks建的模型,结构比较简单,仅两个自由度,一个在头部,用于抬头和低头,一个在身体,用于转身。

如图为头部剖视图,里面放置了K210模块和舵机,灰色部分是轴承座。

如下图为身体的剖视图,ministm32安置在“心部”,可以说是它的心脏了,下边的舵机用于转动整个机身,再往下则是身体通过轴承与底座连接在一起。

组装







作者:柯技柯乐