基于纳芯微NSHT30和NSM3013A的磁编码器的设计
该项目使用了纳芯微温湿度传感器、角度传感器,实现了磁编码器的设计,它的主要功能为:实现了温湿度的采集和显示,实现了角度的采集和舵机的控制,并且实现了多种模式的控制,比如控制速度进行旋转圈数的计数,位置控制等等。。
标签
纳芯微
NSHT30
WeDesign活动第六期
NSM3013A
Bymyself
更新2024-12-25
66

基于纳芯微NSHT30和NSM3013A的磁编码器的设计

一、项目介绍

本次实现的目标是一个磁编码器(带温湿度检测),磁编码器在电机控制中扮演着至关重要的角色,它利用磁场的变化来测量转动位置、速度和方向,是一种高精度、高可靠性的测量装置。磁编码器可以实现实时监测电机的转速、转轴的圆周方向相对位置等参数,并将这些参数转换为数字脉冲信号,反馈给控制系统,具有高精度的测量能力,能够提供准确的位置和角度信息,磁编码器不易受尘埃和结露影响,适用于各种恶劣环境条件下的测量。同时加入了温湿度检测功能,对使用环境进行初步的监测。

二、设计思路

根据本次的实现的目标,我们需要完成一下工作:

1、磁编码器,顾名思义需要实现基于磁力的角度传感器采集,这里我们选用的是NSM3013A-Q1SPR,注意这里提供的是芯片;

2、温湿度检测,可以对磁编码器的使用环境进行监测,这里我们选用的是NSHT30-QDNR,注意这里提供的也是芯片;

3、本次主控使用指定用板带屏12指神探,用于各种传感器的采集和电机的控制;

根据以上的内容,我们详细介绍一下所使用的主要物料:

NSM3013A-Q1SPR:NSM30是一种非接触式旋转角度传感器,在 -40°C至 125°C的环境温度范围内支持 360°旋转角度的精确测量。该芯片基于集成霍尔元件阵列,将两极磁铁的角度位置信息通过内部 DSP解算,将角度信息转化成模拟电压, PWM UVW等各种输出。NSM30提供 SPI和 OWI接口 进行信号路径配置。用户可根据需求对 NSM30的输出特性进行配置(例如输出接口,增益, 钳位电平,滤波等 。该芯片支持3.3V 5V供电电压(不同供电版本)。

NSHT30-QDNR:NSHT30是一种具有IPC接口的相对湿度和温度传感器。它是基于NOVOSENSE新湿度和温度平台的CMOSMEMS传感器芯片。NSHT30将电容式相对湿度传感器、CMOS温度传感器、信号处理器和高速l2C接口集成在单个芯片中,并封装在小型DFN封装中。DEN封装的外形长度仅为2.5毫米,宽度为2.5毫米,高度为0.9毫米。这使得NSHT30可以更广泛地集成到各种应用中。此外,12C接口具有两个独特的可选IPC地址和高达1 MHz的通信速度,宽电源电压范围从2 V到5.5 V,使NSHT30更兼容于各种应用环境。


0

带屏12指神探带屏版的12指神探,它是在原板基础上,配备了一块240*240分辨率的LCD彩屏以及两个可程控按键和一个拨轮,丰富了人机交互功能,方便信息观察、界面切换等使用方式。此外还配备了白色外壳,精心设计的包装不仅使板卡日常使用时更加美观也便于板卡的站立以及使用安全。这个模块是通过Type C的USB接口提供供电、下载以及通信的功能,板上有5V转3.3V,最高支持800mA的电压变换器,在12根引脚上也将5V和3.3V引出,方便对其它外设板供电。采用树莓派Pico核心芯片RP2040,可以运行到133MHz,板子可以通过TYPE-C接口用于供电和数据传输,一个boot按键用于进入boot模式,两个可程控按键和一个拨轮用于自定义功能,搭载240*240分辨率的LCD彩屏,通过SPI接口进行通信,控制器为常用的ST7789芯片,例程丰富便于开发,扩展接口包含5v、3.3v输出、GND、9个GPIO,可同时使能最多三个通道ADC。

舵机:本次主要进行功能演示,使用360度舵机进行控制展示,主要的控制对象

总体框图如下:

image.png

三、素材的搜集

本次提供的芯片在活动中有对应的芯片资料以及应用手册,这方面已经给大家提供了,在进行板子的设计的时候,芯片资料是非常重要的,一些重要参数和注意事项一般都会有标注,具体到芯片的型号,不同的型号在一些具体的参数上是有所不同的。而应用手册在后期的程序设计至关重要,对一些寄存器的解读和操作方法会有一些指导,让我们快速入门。下面是我们在资料中搜索出对于我们有用的芯片信息。

NSM3013A-Q1SPR:

封装:SOP8

供电:5V

支持OWI、模拟输出、PWM输出、UVW输出、SON功能

典型电路
0

NSHT30-QDNR:

封装:DFN

供电:2.0到5.5V

典型电路:
0

四,结构设计

通过以上内容实际上我们就可以进行原理图的绘制了,PCB如果没有特殊要求的话也是可以的,不过我们在进行需要考虑一个结构的问题,如果是简单的测试版就不用考虑,但是我们这次主要实现的是磁编码,需要磁铁的配合使用,还用舵机的控制固定,这些都需要进行结构支持,所有我们需要制作一版测试结构。

结构的重点是舵机、磁铁、芯片的同轴,也就是多舵机在转动的时候,磁铁要和他同步转动且要同轴,这样磁铁才能实现基于中心轴的转动,同轴才能使角度检测精准,分布均匀。

为此我设计了一个测试结构,分别包括舵机基座、舵机端接头、带齿轮转动连接轴,轴承连接轴、磁铁接头、磁编码仓等结构,通过3D打印,另外还需要购买轴承、磁铁(10mm和5mm尺寸各一个)。

磁铁接头就是为了更换不同尺寸磁铁进行测试,毕竟设计时候不知道效果如何,方便更换;轴承是保证长度传导过程中同轴的重要结构。

3D结构通过Rhinoceros 5.0软件绘制,效果如下图:
0

在3D打印的时候发现舵机基座出现收缩变形,手动调整(硬掰)后效果还可以,能够保证同轴:


0
0

实际物品比想象中要小,有一些卡扣会出现过小变形的情况,不过通过手动转动舵机,整体转动还是挺丝滑的,同时也可以给出PCB的限高图,进行下一步原理图和PCB设计。

五、原理图以及PCB绘制

我们在上一部分的结构设计中获取到了关键内容就是我们PCB的限高图,现在我们就开始使用KiCad进行原理图和PCB的绘制。

原理图的首版我们直接使用的推荐电路,而且两个传感器的共同电压支持的是5V,连接座选用的都是常用的PH2.0,所以设计如下:


0

并根据该版原理图绘制PCB:


0

还说这个覆铜如何显示填充效果呢?通过快捷键刷新也不行,所以只能这样,通过3D效果查看时已经覆铜成功的,这个是是第一版,暂定为1.0.在等待PCB打样的过程中,我想到了一个问题开发板的接口使用的是3.3V,这就对数字接口以及模拟接口都有了显得要求,我们使用5V都使用5V供电就不合适了,其中角度传感器只支持5V供电,且输出也是可以达到5V的,在进行模拟量输出的时候需要一个分压。而温湿度传感器的数字接口最好是3.3V的,所以只能重新设计1.1版本,将两个接口的电分开,温湿度传感器使用3.3V供电,而角度传感器使用5V供电,并且角度传感器需要有个分压,才可以让开发板正常采集,升级后的原理图如下:


0

PCB如下:


0

不过PCB打样打的还是1.0版本,我们就需要进行一下修改,只需要将共同连接的电源线修改即可,分压部分采用的都是0603封装,直接相关阻容更换就可以实现和1.1一样得功能,附件分享的直接是1.1版本的,看一看实物:


0

到这里我们的硬件部分基本准备完成了,如下图:


0

六、软件设计

软件设计开始前,我们需要评估本次都需要设计哪些功能,这次选用的是360度舵机,其控制方式是通过PWM进行,到这里感觉好像好缺一个转接板,毕竟不同的供电方式不同,还有很多共线的地方,例如接地,咱们先通过面包板连接,后续还是要做一个转接板更方便。

需求分析:

外部:

数字接口传感器(IIC);

模拟接口采集(ADC);

舵机控制(PWM);

内部:

屏幕显示及刷新;

按键采集

通过现有器件我们需要实现一下功能:

可以通过按键设置工作模式,例如计数模式,可以检测多件转了多少圈;位置模式,将舵机固定在规定的角度,改变设置角度后动作;可以控制舵机的转速等等。

1、温湿度传感器驱动代码实现

实际上和nht30是相同的,我们进行驱动的设计主要是需要使用一个sht30.py的固件,在main中使用主要如下:

from machine import Pin,I2C
import test.sht30 as NSHT30

i2c = I2C(0, sda=Pin(20),scl=Pin(21), freq=100000)
sht = NSHT30.SHT30(i2c,0,0,0x44) # 0x44是模块的I2C地址

temp,huml = sht.measure()

主要包括固件的加载和初始化(包括IIC接口和期间的),然后就可以在循环中获取数据了;

2、角度传感器的采集

角度传感器的采集实际上就是ADC采集:

from machine import Pin,ADC

adc = ADC(Pin(28))

NSM3013data = int(adc.read_u16()* 5.5/(65535)*360//5)

依然需要加载支持的库文件,不过adc属于自带的,接下来就是配置引脚,然后就是在循环中采集,我这里进行了一下初步处理,将adc采集值直接转化为角度值。

3、舵机的控制

舵机是通过PWM进行的控制,一般使用50Hz频率,咱们这次用的是360度舵机,控制区间在0.5ms-2.5ma之间:

from machine import Pin,PWM

pwm = Pin(22,Pin.OUT, value=0) #B
hpwm = PWM(pwm)
hpwm.freq(50)
hpwm.duty_u16(4950)#1650-8250 4950中间态

def set_angle(angle):
# 舵机角度与占空比的映射关系(根据实际情况调整)
# 0° -> 2.5% 占空比
# 180° -> 12.5% 占空比
duty = angle*36.667+1650
hpwm.duty_u16(int(duty)) # 转换占空比为0-65535范围

这里我们使用的PWM也是自带的功能,进行初始化配置,之后设置了一个函数进行占空比的配置,将控制区间(0.5-2.5ms)与0-180度控制范围进行了关联。

4、页面的控制

这里一共包括5个页面和5个按键的采集与控制,所有的采集都是实施进行的,页面刷新是控制的,比如进行页面切换进行一次新基础页面的的展示,不同按键操作后进行判断后的处理:

confirm_pressed = False
return_pressed = False
up_pressed = False
down_pressed = False
center_pressed = False

confirm_btn = Pin(5,Pin.IN, Pin.PULL_UP) #B
return_btn = Pin(6,Pin.IN, Pin.PULL_UP) #A
up_btn = Pin(7,Pin.IN, Pin.PULL_UP) #A
center_btn = Pin(8,Pin.IN, Pin.PULL_UP) #A
down_btn = Pin(9,Pin.IN, Pin.PULL_UP) #A

# 参数
SpeedData = 10
CircleCnt = 0
ActiveState = 0
Page_RefreshCnt = 0
TargetAngle = 100 # 目标角度10-300,步进10
constant_speed_started = False
constant_cnt = 0

# 页面索引
WELCOME_PAGE = 0
MENU_PAGE = 1
SPEED_CONTROL_PAGE = 2
POSITION_CONTROL_PAGE = 3
CONSTANT_SPEED_RUN_PAGE = 4

# 当前页面
current_page = WELCOME_PAGE

# 菜单索引
menu_index = 0

def set_angle(angle):
# 舵机角度与占空比的映射关系(根据实际情况调整)
# 0° -> 2.5% 占空比
# 180° -> 12.5% 占空比
duty = angle*36.667+1650
hpwm.duty_u16(int(duty)) # 转换占空比为0-65535范围

def Show_WELCOME_PAGE(closeOropen):
if closeOropen:
display.text(font2, "WELCOME_PAGE", 10, 10)
display.text(font2, "EETREE", 10, 70)
else:
display.text(font2, "WELCOME_PAGE", 10, 10,color=st7789.BLACK)
display.text(font2, "EETREE", 10, 70 ,color=st7789.BLACK)

def Show_MENU_PAGE(closeOropen):
if closeOropen:
display.text(font2, "MENU_PAGE", 10, 10)
display.text(font1, "CONSTANT_SPEED_RUN_PAGE", 40, 50)
display.text(font1, "POSITION_CONTROL_PAGE", 40, 70)
display.text(font1, "SPEED_CONTROL_PAGE", 40, 90)
display.fill_rect(10,50+menu_index*20,20,8,st7789.MAGENTA)
display.text(font1, "Tem: ", 10, 220,st7789.WHITE,st7789.BLUE)
display.text(font1, "Hum: ", 120, 220,st7789.WHITE,st7789.BLUE)
else:
display.text(font2, "MENU_PAGE", 10, 10)
display.fill_rect(10,50,230,60,st7789.BLACK)
display.fill_rect(10,200,220,40,st7789.BLUE)

def Show_SPEED_PAGE(closeOropen):
if closeOropen:
display.text(font2, "SPEED_PAGE", 10, 10)
display.text(font1, "Active :", 10, 50)
display.fill_rect(120,50,20,8,st7789.RED)
display.text(font1, "SpeedData:", 10, 70)
display.text(font1, str(SpeedData), 120, 70,st7789.CYAN)
display.text(font1, "CircleCnt:", 10, 90)
display.text(font1, str(CircleCnt), 120, 90)
else:
display.text(font2, "SPEED_PAGE", 10, 10,st7789.BLACK)
display.text(font1, "Active :", 10, 50,st7789.BLACK)
display.fill_rect(120,50,20,8,st7789.BLACK)
display.text(font1, "SpeedData:", 10, 70,st7789.BLACK)
display.text(font1, str(SpeedData), 120, 70,st7789.BLACK)
display.text(font1, "CircleCnt:", 10, 90,st7789.BLACK)
display.text(font1, str(CircleCnt), 120, 90,st7789.BLACK)

def Show_CONST_PAGE(closeOropen):
if closeOropen:
display.text(font2, "CONST_PAGE", 10, 10)
display.text(font1, "Active :", 10, 50)
display.fill_rect(120,50,20,8,st7789.RED)
display.text(font1, "CircleNeed:", 10, 70)
display.text(font1, "CircleCnt :", 10, 90)
else:
display.text(font2, "CONST_PAGE", 10, 10,st7789.BLACK)
display.text(font1, "Active :", 10, 50,st7789.BLACK)
display.fill_rect(120,50,20,8,st7789.BLACK)
display.text(font1, "CircleNeed:", 10, 70,st7789.BLACK)
display.text(font1, "CircleCnt :", 10, 90,st7789.BLACK)

def Show_POSIT_PAGE(closeOropen):
if closeOropen:
display.text(font2, "POSIT_PAGE", 10, 10)
display.text(font1, "Active :", 10, 50)
display.fill_rect(120,50,20,8,st7789.RED)
display.text(font1, "PositNeed:", 10, 70)
display.text(font1, str(TargetAngle), 120, 70,st7789.CYAN)
display.text(font1, "PositCur :", 10, 90)
display.text(font2, " ", 50, 130)
else:
display.text(font2, "POSIT_PAGE", 10, 10,st7789.BLACK)
display.text(font1, "Active :", 10, 50,st7789.BLACK)
display.fill_rect(120,50,20,8,st7789.BLACK)
display.text(font1, "PositNeed:", 10, 70,st7789.BLACK)
display.text(font1, str(TargetAngle), 120, 70,st7789.BLACK)
display.text(font1, "PositCur :", 10, 90,st7789.BLACK)
display.text(font2, " ", 50, 130)

'''
f_image = open(image_file0, 'rb')

for column in range(1,240):
buf=f_image.read(480)
display.blit_buffer(buf, 1, column, 240, 1)
'''

while True:

# 按键状态更新
confirm_pressed = not confirm_btn.value()
return_pressed = not return_btn.value()
up_pressed = not up_btn.value()
down_pressed = not down_btn.value()
center_pressed = not center_btn.value()

NSM3013data = int(adc.read_u16()* 5.5/(65535)*360//5)
if NSM3013data <=40 and NSM3013data >10 and constant_cnt == 0:
constant_cnt = 1
elif NSM3013data <=110 and NSM3013data >80 and constant_cnt == 1:
constant_cnt = 2
elif NSM3013data <=280 and NSM3013data >250 and constant_cnt == 1:
constant_cnt = 2
elif NSM3013data <=40 and NSM3013data >10 and constant_cnt == 2:
constant_cnt = 1
CircleCnt = CircleCnt + 1



if sht.is_present():
temp,huml = sht.measure() # 测量温湿度
round_temp=round(temp,1)
round_huml=round(huml,1)

if current_page == POSITION_CONTROL_PAGE:
if constant_speed_started:
if NSM3013data >= TargetAngle-10 and NSM3013data <= TargetAngle+10:
set_angle(90)
display.text(font2, str(NSM3013data)+" ", 50, 130)
else:
set_angle(93)

Page_RefreshCnt = Page_RefreshCnt+1
if Page_RefreshCnt >= 10:
Page_RefreshCnt = 0
if current_page == POSITION_CONTROL_PAGE:
if not constant_speed_started:
display.text(font2, str(NSM3013data)+" ", 50, 130)
if current_page == MENU_PAGE:
display.text(font2, str(round_temp), 50, 200,st7789.YELLOW,st7789.BLUE)
display.text(font2, str(round_huml), 160, 200,st7789.YELLOW,st7789.BLUE)
if current_page == SPEED_CONTROL_PAGE:
display.text(font1, str(CircleCnt)+" ", 120, 90)



if current_page == WELCOME_PAGE:
if confirm_pressed or return_pressed or up_pressed or down_pressed or center_pressed:
current_page = MENU_PAGE
Show_WELCOME_PAGE(0)
menu_index = 0
Show_MENU_PAGE(1)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif current_page == MENU_PAGE:
if up_pressed:
display.fill_rect(10,50+menu_index*20,20,8,st7789.BLACK)
menu_index -= 1
if menu_index < 0:
menu_index = 2
display.fill_rect(10,50+menu_index*20,20,8,st7789.MAGENTA)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif down_pressed:
display.fill_rect(10,50+menu_index*20,20,8,st7789.BLACK)
menu_index += 1
if menu_index > 2:
menu_index = 0
display.fill_rect(10,50+menu_index*20,20,8,st7789.MAGENTA)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif confirm_pressed:
if menu_index == 0:
current_page = CONSTANT_SPEED_RUN_PAGE
Show_MENU_PAGE(0)
Show_CONST_PAGE(1)
elif menu_index == 1:
current_page = POSITION_CONTROL_PAGE
Show_MENU_PAGE(0)
Show_POSIT_PAGE(1)
elif menu_index == 2:
current_page = SPEED_CONTROL_PAGE
Show_MENU_PAGE(0)
Show_SPEED_PAGE(1)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif current_page == SPEED_CONTROL_PAGE:
if return_pressed:
hpwm.deinit()
constant_speed_started = False
current_page = MENU_PAGE
Show_SPEED_PAGE(0)
Show_MENU_PAGE(1)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif center_pressed:
if constant_speed_started:
constant_speed_started = False
display.fill_rect(120,50,20,8,st7789.RED)
hpwm.deinit()
else:
constant_speed_started = True
display.fill_rect(120,50,20,8,st7789.GREEN)
set_angle(90)
hpwm.freq(50)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif up_pressed:
SpeedData = SpeedData - 1
if SpeedData <= 1:
SpeedData = 1
if SpeedData >= 100:
SpeedData = 100
display.text(font1, str(SpeedData)+" ", 120, 70,st7789.CYAN)
if constant_speed_started:
set_angle(90+SpeedData)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif down_pressed:
SpeedData = SpeedData + 1
if SpeedData >= 100:
SpeedData = 100
display.text(font1, str(SpeedData)+" ", 120, 70,st7789.CYAN)
if constant_speed_started:
set_angle(90+SpeedData)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif current_page == POSITION_CONTROL_PAGE:
if return_pressed:
hpwm.deinit()
constant_speed_started = False
current_page = MENU_PAGE
Show_POSIT_PAGE(0)
Show_MENU_PAGE(1)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif center_pressed:
if constant_speed_started:
constant_speed_started = False
display.fill_rect(120,50,20,8,st7789.RED)
hpwm.deinit()
else:
constant_speed_started = True
display.fill_rect(120,50,20,8,st7789.GREEN)
set_angle(90)
hpwm.freq(50)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif up_pressed:
TargetAngle = TargetAngle - 10
if TargetAngle <= 10:
SpeTargetAngleedData = 10
display.text(font1, str(TargetAngle)+" ", 120, 70,st7789.CYAN)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif down_pressed:
TargetAngle = TargetAngle + 10
if TargetAngle >= 300:
TargetAngle = 300
display.text(font1, str(TargetAngle)+" ", 120, 70,st7789.CYAN)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif current_page == CONSTANT_SPEED_RUN_PAGE:
if center_pressed:
if constant_speed_started:
constant_speed_started = False
display.fill_rect(120,50,20,8,st7789.RED)
# pwm.deinit() # 停止 PWM 输出
else:
constant_speed_started = True
display.fill_rect(120,50,20,8,st7789.GREEN)
# pwm.init() # 初始化 PWM 输出
time.sleep(0.2) # 延迟一段时间以避免按键的抖动
elif return_pressed:
current_page = MENU_PAGE
Show_CONST_PAGE(0)
Show_MENU_PAGE(1)
time.sleep(0.2) # 延迟一段时间以避免按键的抖动

其实页面的控制显示相对来说要麻烦一点,由于mpy开发,整个所有的循环操作都是在一个里面进行,其实实时性是有一定滞后的,尤其是写页面的时候是最费时间的,所以我们进行了一些优化设计,比如在位置控制页面进行对应角度的实现的时候,就需要进行一下角度数据实时显示的屏蔽,这样才能准确的进入角度区间,不然舵机是一直在旋转的,有可能会越过去。


0

通过上面的程序,我们实现了温湿度的采集和显示,实现了角度的采集和舵机的控制,并且实现了多种模式的控制,比如控制速度进行旋转圈数的计数,位置控制等等。

七、效果展示

下面看一下定位置模式的效果:

上图可以看到,目前还没有开始启动,所以状态是红色的,设定的位置角度参数是100,当前通过磁铁的位置转化过来的位置参数是151,接下来我们启动后看一下:

启动后舵机通过简单的转到来到了目标角度附近,在±5之内都算是合格误差范围内,并稳定在这个地方。

多种模式的详细操作展示可以通过视频进行展示和讲解,欢迎观看!

八、心得感悟

通过本次的活动进一步的了解了纳芯微的高精度传感器,通过本次DIY的设计让我们可以体验一次全设计流程,包括项目的确定,再到芯片的选型,之后进行资料的设计,重中之重的软硬件设计,可以说对整个开过过程进行了一次全面的细致的体验,再次期间也接触了新的画图软件Kicad,体验了便捷的mpy开发方式,可以说接触了很多全新的内容和知识,非常感谢这样的设计活动!

KiCad文件
使用说明
全屏
附件下载
ProDoc_02 NSM3013A角度传感器测试版1.1_2024-11-04.zip
绘制的采集板Kicad工程
20241105_eetree.zip
mpy软件开发工程文件
开发板及芯片参考资料.zip
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号