Skip to content

Commit

Permalink
ds18b20: new module for 1-wire temperature sensor (#3462)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
theopensourcerer committed Feb 2, 2021
1 parent 19397a0 commit 7d4df65
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 2 deletions.
17 changes: 17 additions & 0 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
77 changes: 77 additions & 0 deletions klippy/extras/ds18b20.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion klippy/extras/heaters.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ def lookup_heater(self, heater_name):
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')
Expand Down
3 changes: 2 additions & 1 deletion src/linux/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
274 changes: 274 additions & 0 deletions src/linux/sensor_ds18b20.c
Original file line number Diff line number Diff line change
@@ -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);

0 comments on commit 7d4df65

Please sign in to comment.