2020-06-21 21:57:00 +02:00
// SPDX-License-Identifier: GPL-2.0
/*
* Sensirion SCD30 carbon dioxide sensor serial driver
*
* Copyright ( c ) 2020 Tomasz Duszynski < tomasz . duszynski @ octakon . com >
*/
# include <linux/crc16.h>
# include <linux/device.h>
# include <linux/errno.h>
# include <linux/iio/iio.h>
# include <linux/jiffies.h>
# include <linux/mod_devicetable.h>
# include <linux/module.h>
# include <linux/property.h>
# include <linux/serdev.h>
# include <linux/string.h>
# include <linux/types.h>
# include <asm/unaligned.h>
# include "scd30.h"
# define SCD30_SERDEV_ADDR 0x61
# define SCD30_SERDEV_WRITE 0x06
# define SCD30_SERDEV_READ 0x03
# define SCD30_SERDEV_MAX_BUF_SIZE 17
# define SCD30_SERDEV_RX_HEADER_SIZE 3
# define SCD30_SERDEV_CRC_SIZE 2
# define SCD30_SERDEV_TIMEOUT msecs_to_jiffies(200)
struct scd30_serdev_priv {
struct completion meas_ready ;
char * buf ;
int num_expected ;
int num ;
} ;
static u16 scd30_serdev_cmd_lookup_tbl [ ] = {
[ CMD_START_MEAS ] = 0x0036 ,
[ CMD_STOP_MEAS ] = 0x0037 ,
[ CMD_MEAS_INTERVAL ] = 0x0025 ,
[ CMD_MEAS_READY ] = 0x0027 ,
[ CMD_READ_MEAS ] = 0x0028 ,
[ CMD_ASC ] = 0x003a ,
[ CMD_FRC ] = 0x0039 ,
[ CMD_TEMP_OFFSET ] = 0x003b ,
[ CMD_FW_VERSION ] = 0x0020 ,
[ CMD_RESET ] = 0x0034 ,
} ;
static u16 scd30_serdev_calc_crc ( const char * buf , int size )
{
return crc16 ( 0xffff , buf , size ) ;
}
static int scd30_serdev_xfer ( struct scd30_state * state , char * txbuf , int txsize ,
char * rxbuf , int rxsize )
{
struct serdev_device * serdev = to_serdev_device ( state - > dev ) ;
struct scd30_serdev_priv * priv = state - > priv ;
int ret ;
priv - > buf = rxbuf ;
priv - > num_expected = rxsize ;
priv - > num = 0 ;
ret = serdev_device_write ( serdev , txbuf , txsize , SCD30_SERDEV_TIMEOUT ) ;
if ( ret < 0 )
return ret ;
if ( ret ! = txsize )
return - EIO ;
ret = wait_for_completion_interruptible_timeout ( & priv - > meas_ready , SCD30_SERDEV_TIMEOUT ) ;
if ( ret < 0 )
return ret ;
if ( ! ret )
return - ETIMEDOUT ;
return 0 ;
}
static int scd30_serdev_command ( struct scd30_state * state , enum scd30_cmd cmd , u16 arg ,
void * response , int size )
{
/*
* Communication over serial line is based on modbus protocol ( or rather
* its variation called modbus over serial to be precise ) . Upon
* receiving a request device should reply with response .
*
* Frame below represents a request message . Each field takes
* exactly one byte .
*
* + - - - - - - + - - - - - - + - - - - - + - - - - - + - - - - - - - + - - - - - - - + - - - - - + - - - - - +
* | dev | op | reg | reg | byte1 | byte0 | crc | crc |
* | addr | code | msb | lsb | | | lsb | msb |
* + - - - - - - + - - - - - - + - - - - - + - - - - - + - - - - - - - + - - - - - - - + - - - - - + - - - - - +
*
* The message device replies with depends on the ' op code ' field from
* the request . In case it was set to SCD30_SERDEV_WRITE sensor should
* reply with unchanged request . Otherwise ' op code ' was set to
* SCD30_SERDEV_READ and response looks like the one below . As with
* request , each field takes one byte .
*
* + - - - - - - + - - - - - - + - - - - - - - - + - - - - - - - + - - - - - + - - - - - - - + - - - - - + - - - - - +
* | dev | op | num of | byte0 | . . . | byteN | crc | crc |
* | addr | code | bytes | | | | lsb | msb |
* + - - - - - - + - - - - - - + - - - - - - - - + - - - - - - - + - - - - - + - - - - - - - + - - - - - + - - - - - +
*/
char txbuf [ SCD30_SERDEV_MAX_BUF_SIZE ] = { SCD30_SERDEV_ADDR } ,
rxbuf [ SCD30_SERDEV_MAX_BUF_SIZE ] ;
int ret , rxsize , txsize = 2 ;
char * rsp = response ;
u16 crc ;
put_unaligned_be16 ( scd30_serdev_cmd_lookup_tbl [ cmd ] , txbuf + txsize ) ;
txsize + = 2 ;
if ( rsp ) {
txbuf [ 1 ] = SCD30_SERDEV_READ ;
if ( cmd = = CMD_READ_MEAS )
/* number of u16 words to read */
put_unaligned_be16 ( size / 2 , txbuf + txsize ) ;
else
put_unaligned_be16 ( 0x0001 , txbuf + txsize ) ;
txsize + = 2 ;
crc = scd30_serdev_calc_crc ( txbuf , txsize ) ;
put_unaligned_le16 ( crc , txbuf + txsize ) ;
txsize + = 2 ;
rxsize = SCD30_SERDEV_RX_HEADER_SIZE + size + SCD30_SERDEV_CRC_SIZE ;
} else {
if ( ( cmd = = CMD_STOP_MEAS ) | | ( cmd = = CMD_RESET ) )
arg = 0x0001 ;
txbuf [ 1 ] = SCD30_SERDEV_WRITE ;
put_unaligned_be16 ( arg , txbuf + txsize ) ;
txsize + = 2 ;
crc = scd30_serdev_calc_crc ( txbuf , txsize ) ;
put_unaligned_le16 ( crc , txbuf + txsize ) ;
txsize + = 2 ;
rxsize = txsize ;
}
ret = scd30_serdev_xfer ( state , txbuf , txsize , rxbuf , rxsize ) ;
if ( ret )
return ret ;
switch ( txbuf [ 1 ] ) {
case SCD30_SERDEV_WRITE :
if ( memcmp ( txbuf , rxbuf , txsize ) ) {
dev_err ( state - > dev , " wrong message received \n " ) ;
return - EIO ;
}
break ;
case SCD30_SERDEV_READ :
if ( rxbuf [ 2 ] ! = ( rxsize - SCD30_SERDEV_RX_HEADER_SIZE - SCD30_SERDEV_CRC_SIZE ) ) {
dev_err ( state - > dev , " received data size does not match header \n " ) ;
return - EIO ;
}
rxsize - = SCD30_SERDEV_CRC_SIZE ;
crc = get_unaligned_le16 ( rxbuf + rxsize ) ;
if ( crc ! = scd30_serdev_calc_crc ( rxbuf , rxsize ) ) {
dev_err ( state - > dev , " data integrity check failed \n " ) ;
return - EIO ;
}
rxsize - = SCD30_SERDEV_RX_HEADER_SIZE ;
memcpy ( rsp , rxbuf + SCD30_SERDEV_RX_HEADER_SIZE , rxsize ) ;
break ;
default :
dev_err ( state - > dev , " received unknown op code \n " ) ;
return - EIO ;
}
return 0 ;
}
static int scd30_serdev_receive_buf ( struct serdev_device * serdev ,
const unsigned char * buf , size_t size )
{
2021-02-09 22:13:15 +01:00
struct iio_dev * indio_dev = serdev_device_get_drvdata ( serdev ) ;
2020-06-21 21:57:00 +02:00
struct scd30_serdev_priv * priv ;
struct scd30_state * state ;
int num ;
if ( ! indio_dev )
return 0 ;
state = iio_priv ( indio_dev ) ;
priv = state - > priv ;
/* just in case sensor puts some unexpected bytes on the bus */
if ( ! priv - > buf )
return 0 ;
if ( priv - > num + size > = priv - > num_expected )
num = priv - > num_expected - priv - > num ;
else
num = size ;
memcpy ( priv - > buf + priv - > num , buf , num ) ;
priv - > num + = num ;
if ( priv - > num = = priv - > num_expected ) {
priv - > buf = NULL ;
complete ( & priv - > meas_ready ) ;
}
return num ;
}
static const struct serdev_device_ops scd30_serdev_ops = {
. receive_buf = scd30_serdev_receive_buf ,
. write_wakeup = serdev_device_write_wakeup ,
} ;
static int scd30_serdev_probe ( struct serdev_device * serdev )
{
struct device * dev = & serdev - > dev ;
struct scd30_serdev_priv * priv ;
int irq , ret ;
priv = devm_kzalloc ( dev , sizeof ( * priv ) , GFP_KERNEL ) ;
if ( ! priv )
return - ENOMEM ;
init_completion ( & priv - > meas_ready ) ;
serdev_device_set_client_ops ( serdev , & scd30_serdev_ops ) ;
ret = devm_serdev_device_open ( dev , serdev ) ;
if ( ret )
return ret ;
serdev_device_set_baudrate ( serdev , 19200 ) ;
serdev_device_set_flow_control ( serdev , false ) ;
ret = serdev_device_set_parity ( serdev , SERDEV_PARITY_NONE ) ;
if ( ret )
return ret ;
irq = fwnode_irq_get ( dev_fwnode ( dev ) , 0 ) ;
return scd30_probe ( dev , irq , KBUILD_MODNAME , priv , scd30_serdev_command ) ;
}
static const struct of_device_id scd30_serdev_of_match [ ] = {
{ . compatible = " sensirion,scd30 " } ,
{ }
} ;
MODULE_DEVICE_TABLE ( of , scd30_serdev_of_match ) ;
static struct serdev_device_driver scd30_serdev_driver = {
. driver = {
. name = KBUILD_MODNAME ,
. of_match_table = scd30_serdev_of_match ,
. pm = & scd30_pm_ops ,
} ,
. probe = scd30_serdev_probe ,
} ;
module_serdev_device_driver ( scd30_serdev_driver ) ;
MODULE_AUTHOR ( " Tomasz Duszynski <tomasz.duszynski@octakon.com> " ) ;
MODULE_DESCRIPTION ( " Sensirion SCD30 carbon dioxide sensor serial driver " ) ;
MODULE_LICENSE ( " GPL v2 " ) ;