ds18b20: new module for 1-wire temperature sensor (#3462)
Initial commit of code to support 1-wire (Dallas) sensors such as the DS18B20. Requires Linux kernel drivers to create a file in /sysfs which is read by this module, and temperature typically returned to a temperature_fan. Signed-off-by: Alan Lord <alanslists@gmail.com> Signed-off-by: Josh Headapohl <joshhead@gmail.com>
This commit is contained in:
parent
19397a0a2b
commit
7d4df65920
|
@ -2153,6 +2153,23 @@ CPU temperature from the Raspberry Pi running the host software.
|
|||
sensor_type: rpi_temperature
|
||||
```
|
||||
|
||||
## DS18B20 temperature sensor
|
||||
|
||||
DS18B20 is a 1-wire (w1) digital temperature sensor. Note that this sensor is not intended for use with extruders and heater beds, but rather for monitoring ambient temperature (C). These sensors have range up to 125 C, so are usable for e.g. chamber temperature monitoring. They can also function as simple fan/heater controllers. DS18B20 sensors are only supported on the "host mcu", e.g. the Raspberry Pi. The w1-gpio Linux kernel module must be installed.
|
||||
|
||||
```
|
||||
sensor_type: DS18B20
|
||||
serial_no:
|
||||
# Each 1-wire device has a unique serial number used to identify the device,
|
||||
# usually in the format 28-031674b175ff. This parameter must be provided.
|
||||
# Attached 1-wire devices can be listed using the following Linux command:
|
||||
# ls /sys/bus/w1/devices/
|
||||
#ds18_report_time:
|
||||
# Interval in seconds between readings. Default is 3.0, with a minimum of 1.0
|
||||
#sensor_mcu:
|
||||
# The micro-controller to read from. Must be the host_mcu
|
||||
```
|
||||
|
||||
# Fans
|
||||
|
||||
## [fan]
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
# Support for 1-wire based temperature sensors
|
||||
#
|
||||
# Copyright (C) 2020 Alan Lord <alanslists@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
import mcu
|
||||
|
||||
DS18_REPORT_TIME = 3.0
|
||||
# Temperature can be sampled at any time but conversion time is ~750ms, so
|
||||
# setting the time too low will not make the reports come faster.
|
||||
DS18_MIN_REPORT_TIME = 1.0
|
||||
|
||||
class DS18B20:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.name = config.get_name().split()[-1]
|
||||
self.sensor_id = config.get("serial_no")
|
||||
self.temp = self.min_temp = self.max_temp = 0.0
|
||||
self._report_clock = 0
|
||||
self.report_time = config.getfloat(
|
||||
'ds18_report_time',
|
||||
DS18_REPORT_TIME,
|
||||
minval=DS18_MIN_REPORT_TIME
|
||||
)
|
||||
self._mcu = mcu.get_printer_mcu(self.printer, config.get('sensor_mcu'))
|
||||
self.oid = self._mcu.create_oid()
|
||||
self._mcu.register_response(self._handle_ds18b20_response,
|
||||
"ds18b20_result", self.oid)
|
||||
self._mcu.register_config_callback(self._build_config)
|
||||
|
||||
def _build_config(self):
|
||||
self._mcu.add_config_cmd("config_ds18b20 oid=%d serial=%s" % (self.oid,
|
||||
self.sensor_id.encode("hex")))
|
||||
|
||||
clock = self._mcu.get_query_slot(self.oid)
|
||||
self._report_clock = self._mcu.seconds_to_clock(self.report_time)
|
||||
self._mcu.add_config_cmd("query_ds18b20 oid=%d clock=%u rest_ticks=%u"
|
||||
" min_value=%d max_value=%d" % (
|
||||
self.oid, clock, self._report_clock,
|
||||
self.min_temp * 1000, self.max_temp * 1000), is_init=True)
|
||||
|
||||
def _handle_ds18b20_response(self, params):
|
||||
temp = params['value'] / 1000.0
|
||||
|
||||
if temp < self.min_temp or temp > self.max_temp:
|
||||
self.printer.invoke_shutdown(
|
||||
"DS18B20 temperature %0.1f outside range of %0.1f:%.01f"
|
||||
% (temp, self.min_temp, self.max_temp))
|
||||
|
||||
next_clock = self._mcu.clock32_to_clock64(params['next_clock'])
|
||||
last_read_clock = next_clock - self._report_clock
|
||||
last_read_time = self._mcu.clock_to_print_time(last_read_clock)
|
||||
self._callback(last_read_time, temp)
|
||||
|
||||
def setup_minmax(self, min_temp, max_temp):
|
||||
self.min_temp = min_temp
|
||||
self.max_temp = max_temp
|
||||
|
||||
def fault(self, msg):
|
||||
self.printer.invoke_async_shutdown(msg)
|
||||
|
||||
def get_report_time_delta(self):
|
||||
return self.report_time
|
||||
|
||||
def setup_callback(self, cb):
|
||||
self._callback = cb
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return {
|
||||
'temperature': self.temp,
|
||||
}
|
||||
|
||||
def load_config(config):
|
||||
# Register sensor
|
||||
pheaters = config.get_printer().load_object(config, "heaters")
|
||||
pheaters.add_sensor_factory("DS18B20", DS18B20)
|
|
@ -265,7 +265,8 @@ class PrinterHeaters:
|
|||
def setup_sensor(self, config):
|
||||
modules = ["thermistor", "adc_temperature", "spi_temperature",
|
||||
"bme280", "htu21d", "lm75", "rpi_temperature",
|
||||
"temperature_mcu"]
|
||||
"temperature_mcu", "ds18b20"]
|
||||
|
||||
for module_name in modules:
|
||||
self.printer.load_object(config, module_name)
|
||||
sensor_type = config.get('sensor_type')
|
||||
|
|
|
@ -5,8 +5,9 @@ dirs-y += src/linux src/generic
|
|||
src-y += linux/main.c linux/timer.c linux/console.c linux/watchdog.c
|
||||
src-y += linux/pca9685.c linux/spidev.c linux/analog.c linux/hard_pwm.c
|
||||
src-y += linux/i2c.c linux/gpio.c generic/crc16_ccitt.c generic/alloc.c
|
||||
src-y += linux/sensor_ds18b20.c
|
||||
|
||||
CFLAGS_klipper.elf += -lutil
|
||||
CFLAGS_klipper.elf += -lutil -lpthread
|
||||
|
||||
flash: $(OUT)klipper.elf
|
||||
@echo " Flashing"
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
// Communicate with a DS18B20 temperature sensor on linux
|
||||
//
|
||||
// Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
//
|
||||
// This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
#include <fcntl.h> // open
|
||||
#include <stdio.h> // snprintf
|
||||
#include <stdlib.h> // atof
|
||||
#include <string.h> // memchr
|
||||
#include <unistd.h> // read
|
||||
#include <pthread.h> // pthread_create
|
||||
#include <time.h> // clock_gettime
|
||||
#include "basecmd.h" // oid_alloc
|
||||
#include "board/irq.h" // irq_disable
|
||||
#include "board/misc.h" // output
|
||||
#include "command.h" // DECL_COMMAND
|
||||
#include "internal.h" // report_errno
|
||||
#include "sched.h" // DECL_SHUTDOWN
|
||||
|
||||
#define W1_READ_TIMEOUT_SEC 5
|
||||
|
||||
// Status of a sensor
|
||||
enum {
|
||||
W1_IDLE = 0, // No read requested yet
|
||||
W1_READ_REQUESTED = 1, // Reading or waiting to read
|
||||
W1_READY = 2, // Read complete, waiting to report
|
||||
W1_ERROR = 3, // Request shutdown
|
||||
};
|
||||
|
||||
enum {
|
||||
TS_PENDING = 1,
|
||||
};
|
||||
|
||||
struct ds18_s {
|
||||
struct timer timer;
|
||||
uint32_t rest_time;
|
||||
int32_t min_value, max_value;
|
||||
uint8_t flags;
|
||||
|
||||
// Set by main thread in configuration phase.
|
||||
// Should only be accessed by reader thread after configuration.
|
||||
int fd;
|
||||
|
||||
// Used for guarding shared members.
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t cond;
|
||||
|
||||
// Protect all reads/writes to the following members using the mutex
|
||||
// once reader thread is initialized.
|
||||
int temperature;
|
||||
struct timespec request_time;
|
||||
uint8_t status;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Lock ds18_s mutex, set error status and message, unlock mutex.
|
||||
static void
|
||||
locking_set_read_error(struct ds18_s *d, const char *error)
|
||||
{
|
||||
pthread_mutex_lock(&d->lock);
|
||||
d->error = error;
|
||||
d->status = W1_ERROR;
|
||||
pthread_mutex_unlock(&d->lock);
|
||||
}
|
||||
|
||||
// The kernel interface to DS18B20 sensors is a sysfs entry that blocks for
|
||||
// around 750ms when read. Most of this is idle time waiting for the result
|
||||
// to be ready. Read in a separate thread in order to avoid blocking time-
|
||||
// sensitive work.
|
||||
static void *
|
||||
reader_start_routine(void *param) {
|
||||
struct ds18_s *d = param;
|
||||
for (;;) {
|
||||
// Wait for requests to read temperature sensors
|
||||
pthread_mutex_lock(&d->lock);
|
||||
while (d->status != W1_READ_REQUESTED) {
|
||||
pthread_cond_wait(&d->cond, &d->lock);
|
||||
}
|
||||
pthread_mutex_unlock(&d->lock);
|
||||
|
||||
// Read temp.
|
||||
// The temperature data is at the end of the report, after a "t=".
|
||||
// Example (3.062 degrees C):
|
||||
//
|
||||
// 31 00 4b 46 7f ff 0c 10 77 : crc=77 YES
|
||||
// 31 00 4b 46 7f ff 0c 10 77 t=3062
|
||||
char data[128];
|
||||
int ret = read(d->fd, data, sizeof(data)-1);
|
||||
if (ret < 0) {
|
||||
report_errno("read DS18B20", ret);
|
||||
locking_set_read_error(d, "Unable to read DS18B20");
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
data[ret] = '\0';
|
||||
char *temp_string = strstr(data, "t=");
|
||||
if (temp_string == NULL || temp_string[2] == '\0') {
|
||||
locking_set_read_error(d,
|
||||
"Unable to find temperature value in DS18B20 report");
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
// Don't pass 't' and '=' to atoi
|
||||
temp_string += 2;
|
||||
int val = atoi(temp_string);
|
||||
|
||||
// Store temperature
|
||||
pthread_mutex_lock(&d->lock);
|
||||
d->status = W1_READY;
|
||||
d->temperature = val;
|
||||
pthread_mutex_unlock(&d->lock);
|
||||
|
||||
// Seek file in preparation of next read
|
||||
ret = lseek(d->fd, 0, SEEK_SET);
|
||||
if (ret < 0) {
|
||||
report_errno("seek DS18B20", ret);
|
||||
locking_set_read_error(d, "Unable to seek DS18B20");
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
}
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static struct task_wake ds18_wake;
|
||||
|
||||
static uint_fast8_t
|
||||
ds18_event(struct timer *timer)
|
||||
{
|
||||
struct ds18_s *d = container_of(timer, struct ds18_s, timer);
|
||||
// Trigger task to read and send results
|
||||
sched_wake_task(&ds18_wake);
|
||||
d->flags |= TS_PENDING;
|
||||
d->timer.waketime += d->rest_time;
|
||||
return SF_RESCHEDULE;
|
||||
}
|
||||
|
||||
void
|
||||
command_config_ds18b20(uint32_t *args)
|
||||
{
|
||||
// Open kernel port
|
||||
uint8_t serial_len = args[1];
|
||||
uint8_t *serial = (void*)(size_t)args[2];
|
||||
if (memchr(serial, '/', serial_len))
|
||||
goto fail1;
|
||||
char fname[56];
|
||||
snprintf(fname, sizeof(fname), "/sys/bus/w1/devices/%.*s/w1_slave"
|
||||
, serial_len, serial);
|
||||
int fd = open(fname, O_RDONLY|O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
report_errno("open ds18", fd);
|
||||
goto fail2;
|
||||
}
|
||||
|
||||
struct ds18_s *d = oid_alloc(args[0], command_config_ds18b20, sizeof(*d));
|
||||
d->timer.func = ds18_event;
|
||||
d->fd = fd;
|
||||
d->status = W1_IDLE;
|
||||
int ret;
|
||||
ret = pthread_mutex_init(&d->lock, NULL);
|
||||
if (ret)
|
||||
goto fail3;
|
||||
ret = pthread_cond_init(&d->cond, NULL);
|
||||
if (ret)
|
||||
goto fail4;
|
||||
|
||||
pthread_t reader_tid; // Not used
|
||||
ret = pthread_create(&reader_tid, NULL, reader_start_routine, d);
|
||||
if (ret)
|
||||
goto fail5;
|
||||
|
||||
return;
|
||||
fail1:
|
||||
shutdown("Invalid DS18B20 serial id, must not contain '/'");
|
||||
fail2:
|
||||
shutdown("Invalid DS18B20 serial id, could not open for reading");
|
||||
fail3:
|
||||
shutdown("Could not start DS18B20 reader thread (mutex init)");
|
||||
fail4:
|
||||
shutdown("Could not start DS18B20 reader thread (cond init)");
|
||||
fail5:
|
||||
shutdown("Could not start DS18B20 reader thread");
|
||||
}
|
||||
DECL_COMMAND(command_config_ds18b20, "config_ds18b20 oid=%c serial=%*s");
|
||||
|
||||
void
|
||||
command_query_ds18b20(uint32_t *args)
|
||||
{
|
||||
struct ds18_s *d = oid_lookup(args[0], command_config_ds18b20);
|
||||
|
||||
sched_del_timer(&d->timer);
|
||||
d->timer.waketime = args[1];
|
||||
d->rest_time = args[2];
|
||||
if (! d->rest_time)
|
||||
return;
|
||||
d->min_value = args[3];
|
||||
d->max_value = args[4];
|
||||
sched_add_timer(&d->timer);
|
||||
}
|
||||
DECL_COMMAND(command_query_ds18b20,
|
||||
"query_ds18b20 oid=%c clock=%u rest_ticks=%u"
|
||||
" min_value=%i max_value=%i");
|
||||
|
||||
// Report temperature if ready, and set back to pending.
|
||||
static void
|
||||
ds18_send_and_request(struct ds18_s *d, uint32_t next_begin_time, uint8_t oid)
|
||||
{
|
||||
struct timespec request_time;
|
||||
int ret = clock_gettime(CLOCK_MONOTONIC, &request_time);
|
||||
if (ret == -1) {
|
||||
report_errno("get monotonic clock time", ret);
|
||||
try_shutdown("Error getting monotonic clock time");
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&d->lock);
|
||||
if (d->status == W1_ERROR) {
|
||||
// try_shutdown expects a static string. Output the specific error,
|
||||
// then shut down with a generic error.
|
||||
output("Error: %s", d->error);
|
||||
pthread_mutex_unlock(&d->lock);
|
||||
try_shutdown("Error reading DS18B20 sensor");
|
||||
return;
|
||||
} else if (d->status == W1_IDLE) {
|
||||
// This happens the first time requesting a temperature.
|
||||
// Nothing to report yet.
|
||||
d->request_time = request_time;
|
||||
d->status = W1_READ_REQUESTED;
|
||||
} else if (d->status == W1_READY) {
|
||||
// Report the previous temperature and request a new one.
|
||||
sendf("ds18b20_result oid=%c next_clock=%u value=%i"
|
||||
, oid, next_begin_time, d->temperature);
|
||||
if (d->temperature < d->min_value || d->temperature > d->max_value) {
|
||||
pthread_mutex_unlock(&d->lock);
|
||||
try_shutdown("DS18B20 out of range");
|
||||
return;
|
||||
}
|
||||
d->request_time = request_time;
|
||||
d->status = W1_READ_REQUESTED;
|
||||
} else if (d->status == W1_READ_REQUESTED) {
|
||||
// Reader thread is already reading (or will be soon).
|
||||
// This can happen if two queries come in quick enough
|
||||
// succession. Wait for the existing read to finish.
|
||||
// This could also happen if the reader thread has hung. In that case,
|
||||
// shut down the MCU. To tell the difference, see if the request time
|
||||
// is too far in the past.
|
||||
if (request_time.tv_sec - d->request_time.tv_sec > W1_READ_TIMEOUT_SEC)
|
||||
{
|
||||
pthread_mutex_unlock(&d->lock);
|
||||
try_shutdown("DS18B20 sensor didn't respond in time");
|
||||
return;
|
||||
}
|
||||
}
|
||||
pthread_cond_signal(&d->cond);
|
||||
pthread_mutex_unlock(&d->lock);
|
||||
}
|
||||
|
||||
// task to read temperature and send response
|
||||
void
|
||||
ds18_task(void)
|
||||
{
|
||||
if (!sched_check_wake(&ds18_wake))
|
||||
return;
|
||||
uint8_t oid;
|
||||
struct ds18_s *d;
|
||||
foreach_oid(oid, d, command_config_ds18b20) {
|
||||
if (!(d->flags & TS_PENDING))
|
||||
continue;
|
||||
irq_disable();
|
||||
uint32_t next_begin_time = d->timer.waketime;
|
||||
d->flags &= ~TS_PENDING;
|
||||
irq_enable();
|
||||
ds18_send_and_request(d, next_begin_time, oid);
|
||||
}
|
||||
}
|
||||
DECL_TASK(ds18_task);
|
Loading…
Reference in New Issue