项目介绍
一、项目功能介绍
本项目基于搭配带屏12指神探的传感器扩展板实现了一个恒温控制系统。该系统实现的具体功能如下:
温度测量:通过连接的温度传感器实时测量环境温度;
温度设定:通过按钮设定所需的目标温度;
实时显示:在LCD屏幕上实时显示当前环境温度、设定的目标温度;
温度调节:根据PID控制算法输出的控制信号,控制加热电阻的加热功率,以调节环境温度。环境温度小于设定温度时在PID的控制下进行加热,大于设定温度时停止加热。
二、设计思路
根据试图实现的功能,可以将项目分为以下几个模块:
温度读取模块:从NSHT30温度传感器读取当前温度数据,将读取的温度数据传递给控制模块;
PID控制模块:根据当前温度和目标温度计算PID控制器的输出。通过PID控制加热电阻的工作状态,使温度稳定在目标温度附近;
LCD显示模块:更新LCD屏幕上显示的当前温度、目标温度;
用户交互模块:可以添加按钮或旋钮等输入设备,用于用户设置目标温度。根据用户的输入更新目标温度,并将其传递给主控制模块;
主控制模块:设置目标温度,调用温度读取模块读取当前温度,并根据当前温度和目标温度调用PID控制模块控制加热电阻。调用LCD显示模块更新显示;
主循环模块:在主循环中不断调用主控制模块,实现温度控制的闭环反馈。可以设置适当的延迟,以控制温度更新的频率。
三、硬件框图
带屏版的12指神探硬件介绍:
带屏版的12指神探在原板基础上,配备了一块240*240分辨率的LCD彩屏以及两个可程控按键和一个拨轮,丰富了人机交互功能,方便信息观察、界面切换等使用方式。模块通过Type C的USB接口提供供电、下载以及通信的功能,板上有5V转3.3V,最高支持800mA的电压变换器,在12根引脚上也将5V和3.3V引出,方便对其它外设板供电。
主控芯片采用树莓派Pico核心芯片RP2040,配置为: 双Arm Cortex M0+内核,可以运行到133MHz;264KBSRAM,板卡上外扩2MBFlash;性能强大、高度灵活的可编程IO(PIO)可用于高速数字接口;拥有2个UART、2个SPI、2个I2C、16个PWM通道以及4路12位精度ADC;支持MicroPython、C、C++编程;拖拽UF2固件至U盘的方式烧录固件,方便快捷。
扩展板硬件介绍:
作为12指神探的传感器扩展板,接口完全适配,正确方向插入后即可使用。扩展板搭载了几款常见传感器和功能模块,包括为初学者准备的麦克风、蜂鸣器、红外收发、霍尔效应开关、加热电阻,为进阶操作准备的温湿度传感器、六轴传感器、接近/环境光/IR传感器、颜色传感器。其中温湿度传感器、六轴传感器、接近传感器、颜色传感器可拆卸为单个模块,通过杜邦线等连接线延伸其使用的空间范围。出厂默认传感器正面朝上使用。若需背面朝上使用,则自行焊接排母后按指示方向插入。
四、功能展示
左键按下,设定温度-;右键按下,设定温度+;
温度低于设定温度,加热电阻开始加热:
温度高于设定温度,加热电阻关闭:
五、代码说明
软件流程:
开始 -> 初始化LCD屏幕和温度传感器 -> 设定目标温度
-> 循环:
{ -> 通过按键修改目标温度 -> LCD显示修改后的目标温度 -> 读取当前温度 -> LCD实时显示目标温度 -> 计算PID控制输出 -> 控制加热电阻 }
-> 结束
代码:
1、导入开源库NSHT30.py用于驱动NSHT30温湿度传感器
from machine import Pin, I2C
import time
# I2C address B 0x44 ADDR (pin 2) connected to GND
DEFAULT_I2C_ADDRESS = 0x44
class SHT30:
POLYNOMIAL = 0x131 # P(x) = x^8 + x^5 + x^4 + 1 = 100110001
ALERT_PENDING_MASK = 0x8000 # 15
HEATER_MASK = 0x2000 # 13
RH_ALERT_MASK = 0x0800 # 11
T_ALERT_MASK = 0x0400 # 10
RESET_MASK = 0x0010 # 4
CMD_STATUS_MASK = 0x0002 # 1
WRITE_STATUS_MASK = 0x0001 # 0
# MSB = 0x2C LSB = 0x06 Repeatability = High, Clock stretching = enabled
MEASURE_CMD = b'\x2C\x10'
STATUS_CMD = b'\xF3\x2D'
RESET_CMD = b'\x30\xA2'
CLEAR_STATUS_CMD = b'\x30\x41'
ENABLE_HEATER_CMD = b'\x30\x6D'
DISABLE_HEATER_CMD = b'\x30\x66'
def __init__(self, i2c=None, delta_temp=0, delta_hum=0, i2c_address=None):
if i2c is None:
raise ValueError('An I2C object is required.')
self.i2c = i2c
self.i2c_addr = i2c_address
self.set_delta(delta_temp, delta_hum)
time.sleep_ms(50)
def is_present(self):
"""
Return true if the sensor is correctly conneced, False otherwise
"""
return self.i2c_addr in self.i2c.scan()
def set_delta(self, delta_temp=0, delta_hum=0):
"""
Apply a delta value on the future measurements of temperature and/or humidity
The units are Celsius for temperature and percent for humidity (can be negative values)
"""
self.delta_temp = delta_temp
self.delta_hum = delta_hum
def _check_crc(self, data):
# calculates 8-Bit checksum with given polynomial
crc = 0xFF
for b in data[:-1]:
crc ^= b
for _ in range(8, 0, -1):
if crc & 0x80:
crc = (crc << 1) ^ SHT30.POLYNOMIAL
else:
crc <<= 1
crc_to_check = data[-1]
return crc_to_check == crc
def send_cmd(self, cmd_request, response_size=6, read_delay_ms=100):
"""
Send a command to the sensor and read (optionally) the response
The responsed data is validated by CRC
"""
try:
self.i2c.writeto(self.i2c_addr, cmd_request)
if not response_size:
return
time.sleep_ms(read_delay_ms)
data = self.i2c.readfrom(self.i2c_addr, response_size)
for i in range(response_size // 3):
if not self._check_crc(data[i * 3:(i + 1) * 3]): # pos 2 and 5 are CRC
raise SHT30Error(SHT30Error.CRC_ERROR)
if data == bytearray(response_size):
raise SHT30Error(SHT30Error.DATA_ERROR)
return data
except OSError:
raise SHT30Error(SHT30Error.BUS_ERROR)
except Exception as ex:
raise ex
def clear_status(self):
"""
Clear the status register
"""
return self.send_cmd(SHT30.CLEAR_STATUS_CMD, None)
def reset(self):
"""
Send a soft-reset to the sensor
"""
return self.send_cmd(SHT30.RESET_CMD, None)
def status(self, raw=False):
"""
Get the sensor status register.
It returns a int value or the bytearray(3) if raw==True
"""
data = self.send_cmd(SHT30.STATUS_CMD, 3, read_delay_ms=20)
if raw:
return data
status_register = data[0] << 8 | data[1]
return status_register
def measure(self, raw=False):
"""
If raw==True returns a bytearrya(6) with sensor direct measurement otherwise
It gets the temperature (T) and humidity (RH) measurement and return them.
The units are Celsius and percent
"""
data = self.send_cmd(SHT30.MEASURE_CMD, 6)
if raw:
return data
t_celsius = (((data[0] << 8 | data[1]) * 175) / 0xFFFF) - 45 + self.delta_temp
rh = (((data[3] << 8 | data[4]) * 100.0) / 0xFFFF) + self.delta_hum
return t_celsius, rh
def measure_int(self, raw=False):
"""
Get the temperature (T) and humidity (RH) measurement using integers.
If raw==True returns a bytearrya(6) with sensor direct measurement otherwise
It returns a tuple with 4 values: T integer, T decimal, H integer, H decimal
For instance to return T=24.0512 and RH= 34.662 This method will return
(24, 5, 34, 66) Only 2 decimal digits are returned, .05 becomes 5
Delta values are not applied in this method
The units are Celsius and percent.
"""
data = self.send_cmd(SHT30.MEASURE_CMD, 6)
if raw:
return data
aux = (data[0] << 8 | data[1]) * 175
t_int = (aux // 0xffff) - 45
t_dec = (aux % 0xffff * 100) // 0xffff
aux = (data[3] << 8 | data[4]) * 100
h_int = aux // 0xffff
h_dec = (aux % 0xffff * 100) // 0xffff
return t_int, t_dec, h_int, h_dec
class SHT30Error(Exception):
"""
Custom exception for errors on sensor management
"""
BUS_ERROR = 0x01
DATA_ERROR = 0x02
CRC_ERROR = 0x03
def __init__(self, error_code=None):
self.error_code = error_code
super().__init__(self.get_message())
def get_message(self):
if self.error_code == SHT30Error.BUS_ERROR:
return "Bus error"
elif self.error_code == SHT30Error.DATA_ERROR:
return "Data error"
elif self.error_code == SHT30Error.CRC_ERROR:
return "CRC error"
else:
return "Unknown error"
2、主要代码
导入模块:
导入所需的Python模块,包括time、machine,以及来自machine模块的SPI、Pin、I2C、PWM类,同时导入了自定义的模块test.st7789和test.NSHT30以及字体文件。
import time # 导入时间模块
import machine # 导入机器模块
from machine import SPI, Pin, I2C, PWM # 从机器模块中导入SPI、Pin、I2C、PWM类
import test.st7789 as st7789 # 导入test文件夹下的st7789模块
import test.NSHT30 as NSHT30 # 导入test文件夹下的NSHT30模块
from test.fonts import vga2_8x8 as font1 # 导入test文件夹下的vga2_8x8字体
from test.fonts import vga1_16x32 as font2 # 导入test文件夹下的vga1_16x32字体
LCD配置:
设置LCD相关的参数,如复位引脚、数据/命令引脚、显示宽度和高度等,然后通过SPI初始化LCD显示器。
# 配置LCD
st7789_res = 0 # LCD复位引脚
st7789_dc = 1 # LCD数据/命令引脚
disp_width = 240 # LCD显示宽度
disp_height = 240 # LCD显示高度
spi_sck = Pin(2) # SPI时钟引脚
spi_tx = Pin(3) # SPI传输引脚
spi = SPI(0, baudrate=40000000, polarity=1, phase=1, sck=spi_sck, mosi=spi_tx) # 初始化SPI
display = st7789.ST7789(spi, disp_width, disp_height,
reset=Pin(st7789_res, Pin.OUT),
dc=Pin(st7789_dc, Pin.OUT),
xstart=0, ystart=0, rotation=0) # 初始化LCD显示器
display.fill(st7789.BLACK) # 填充LCD为黑色
display.text(font2, "EETREE", 10, 10) # 在LCD上显示文本"EETREE"
按键和I2C设置:
定义了两个按键的引脚,并设置为输入模式,通过Pin.PULL_UP参数启用了内部上拉电阻。
初始化了I2C总线,设置了时钟和数据引脚,并指定了通信频率。
button1 = Pin(5, Pin.IN, Pin.PULL_UP) # B按键
button2 = Pin(6, Pin.IN, Pin.PULL_UP) # A按键
# 设置I2C总线
scl = Pin(21) # I2C时钟引脚
sda = Pin(20) # I2C数据引脚
i2c = I2C(0, scl=scl, sda=sda, freq=400000) # 初始化I2C总线
传感器初始化:
创建了一个NSHT30对象,传入了I2C总线对象和传感器的参数。
# 创建NSHT30对象
nsht = NSHT30.SHT30(i2c, 0, 0, 0x44)
PID参数和初始化:
设置了PID控制算法的比例系数、积分系数和微分系数,并初始化了累积误差和上一次的误差。
# pid参数
kp = 1000 # 比例系数
ki = 50 # 积分系数
kd = 0.1 # 微分系数
# pid初始化
last_error = 0 # 上一次的误差
integral = 0 # 累积误差
温度和PWM设置:
设置了温度的最大值、最小值和目标温度,并初始化了PWM控制加热电阻的频率。
# 设置温度
temp_max = 120 # 温度最大值
temp_min = 0 # 温度最小值
target_temp = 30 # 目标温度
# 设置PWM
pwm_pin = machine.Pin(22, Pin.OUT) # 加热电阻引脚
pwm = PWM(pwm_pin) # 控制加热电阻的PWM输出
pwm.freq(1000) # 设置PWM频率为1000Hz
主循环:
在一个无限循环中执行以下操作:
检测按键状态,根据按键调整目标温度并显示
# 检测按钮状态,调整目标温度
if button1.value() == 0:
target_temp -= 1
if button2.value() == 0:
target_temp += 1
# 如果目标温度大于最大值,则输出设为最大值
if target_temp > temp_max:
target_temp = temp_max
# 如果目标温度小于最小值,则输出设为最小值
if target_temp < temp_min:
target_temp = temp_min
# 显示目标温度
display.text(font2, "target temp:", 10, 50)
display.text(font2, str(target_temp), 50, 100)
通过传感器获取实时温度数据并显示
其中nsht.measure()函数在NSHT30.py驱动程序中定义,用于获取温度(temp)和湿度(huml)两个数据,我们仅需要温度数据;
time.sleep()函数用于进行缓冲;
# 获取温度数据
temp, huml = nsht.measure()
time.sleep(0.1)
# 显示实时温度
display.text(font2, "present temp:", 10, 150)
display.text(font2, str(temp), 50, 200)
根据PID控制算法计算控制器输出
# pid控制算法
error = target_temp - temp # 误差(设定值与实际值的差值)
integral += error # 累积误差
derivative = error - last_error # 当前误差与上一次误差的差值
output = kp * error + ki * integral + kd * derivative # PID控制器输出
控制加热电阻的工作状态,以调节温度
当温度小于目标温度时,启动加热电阻并根据PID控制器的输出控制加热电阻输出功率,温差减小时输出功率随之减小;温度大于目标温度时,关闭加热电阻;
# 控制加热电阻
if temp < target_temp:
pwm_duty = int(abs(output)) # 将PID输出映射到PWM占空比
# 将PWM占空限制在合理的范围内,避免超出0到65535的范围
if pwm_duty >= 65535:
pwm_duty = 65535
if pwm_duty <= 0:
pwm_duty = 0
time.sleep(0.1)
pwm.duty_u16(pwm_duty) # 控制PWM占空比else:
pwm.duty_u16(0) # 设置PWM占空比为0%,关闭加热电阻
更新误差值,进入下一次循环
time.sleep(0.1)
last_error = error # 更新上一次的误差
六、主要问题
NSHT30温湿度传感器驱动:需要找到并导入合适的驱动程序并使用其中提供的实时温度测量功能;
PID参数设置:PID控制算法中的比例系数(kp)、积分系数(ki)和微分系数(kd)需要通过试验和调整来确定最佳值。比例系数决定了控制器输出与误差的线性关系,如果比例系数过大,系统可能会出现超调(即温度波动较大),反之,系统响应可能会过于迟缓;积分系数用于消除稳态误差,较大的积分系数可以更快地消除稳态误差,但可能会导致超调或振荡;微分系数用于抑制系统的震荡和超调,它可以根据误差的变化率来调整控制器输出,使系统更加稳定。最终选取kp=1000、ki=50、kd=0.1,这组参数既能保持合适的加热速率,使温度稳定在设定温度附近,同时能够较好反映加热电阻功率在PID控制下的变化。
七、未来规划
项目还无法实现手动开启关闭温控系统,未来可以尝试添加相应的控制模块。同时可以继续调试,测量不同参数下PID控制算法的工作效率,选择更合适的参数。