[drivers]: gpios: SN74HC595: Extend to allow for chained shift registers

The current driver has a few limitations:
1. The `ngpios` DT property is fixed at eight.
   Since the SN74HC595 and kin are designed to be
   easily daisychain-able, the upper bound on `ngpios`
   should be limited only by the maximum number of pins
   that Zephyr supports per GPIO port, which is 32.
2. In the case of having no control over the shift register's
   reset input, the device tree node should accept a default
   value to shift into the register(s) during init.
3. There seems to be an assumption that the serial clock
   and load clock are tied together. While this is often the
   case, the device tree node should be more flexible in
   allowing the specification of a separate load clock GPIO pin.
4. The device tree node should also be able to accept a GPIO pin
   to drive the enable input pin of the shift register(s).

This commit addresses all of these issues.

Signed-off-by: Pete Dietl <petedietl@gmail.com>
This commit is contained in:
Pete Dietl
2025-05-29 18:00:10 -07:00
committed by Benjamin Cabé
parent 090130c35b
commit c407fbcfc9
2 changed files with 130 additions and 58 deletions

View File

@@ -15,6 +15,8 @@
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/drivers/gpio/gpio_utils.h>
@@ -26,32 +28,45 @@ LOG_MODULE_REGISTER(gpio_sn74hc595, CONFIG_GPIO_LOG_LEVEL);
#endif
struct gpio_sn74hc595_config {
/* gpio_driver_config needs to be first */
struct gpio_driver_config config;
struct spi_dt_spec bus;
struct gpio_dt_spec enable_gpio;
struct gpio_dt_spec reset_gpio;
uint8_t num_registers;
};
struct gpio_sn74hc595_drv_data {
/* gpio_driver_data needs to be first */
struct gpio_driver_data data;
struct k_mutex lock;
uint8_t output;
uint32_t output;
};
static int sn74hc595_spi_write(const struct device *dev, void *buf, size_t len_bytes)
static int sn74hc595_write(const struct device *dev, uint32_t value)
{
int ret;
uint32_t adjusted_value;
const struct gpio_sn74hc595_config *config = dev->config;
struct gpio_sn74hc595_drv_data *drv_data = dev->data;
__ASSERT(((buf != NULL) || (len_bytes == 0)), "no valid buffer given");
__ASSERT(!k_is_in_isr(), "attempt to access SPI from ISR");
struct spi_buf tx_buf[] = { { .buf = buf, .len = len_bytes } };
const struct spi_buf_set tx = { .buffers = tx_buf, .count = 1 };
adjusted_value = sys_cpu_to_be32(
value << (BITS_PER_BYTE * (sizeof(uint32_t) - config->num_registers)));
return spi_write_dt(&config->bus, &tx);
struct spi_buf tx_buf[] = {
{.buf = (uint8_t *)&adjusted_value, .len = config->num_registers}};
const struct spi_buf_set tx = {.buffers = tx_buf, .count = 1};
ret = spi_write_dt(&config->bus, &tx);
if (ret == 0) {
drv_data->output = value;
}
return ret;
}
static int gpio_sn74hc595_config(const struct device *dev, gpio_pin_t pin, gpio_flags_t flags)
@@ -80,7 +95,7 @@ static int gpio_sn74hc595_port_set_masked_raw(const struct device *dev, uint32_t
{
struct gpio_sn74hc595_drv_data *drv_data = dev->data;
int ret = 0;
uint8_t output;
uint32_t output;
k_mutex_lock(&drv_data->lock, K_FOREVER);
@@ -89,16 +104,11 @@ static int gpio_sn74hc595_port_set_masked_raw(const struct device *dev, uint32_t
if ((drv_data->output & mask) != (mask & value)) {
output = (drv_data->output & ~mask) | (mask & value);
ret = sn74hc595_spi_write(dev, &output, 1U);
if (ret < 0) {
goto unlock;
}
drv_data->output = output;
ret = sn74hc595_write(dev, output);
}
unlock:
k_mutex_unlock(&drv_data->lock);
return ret;
}
@@ -116,21 +126,16 @@ static int gpio_sn74hc595_port_toggle_bits(const struct device *dev, uint32_t ma
{
struct gpio_sn74hc595_drv_data *drv_data = dev->data;
int ret;
uint8_t toggled_output;
uint32_t toggled_output;
k_mutex_lock(&drv_data->lock, K_FOREVER);
toggled_output = drv_data->output ^ mask;
ret = sn74hc595_spi_write(dev, &toggled_output, 1U);
if (ret < 0) {
goto unlock;
}
ret = sn74hc595_write(dev, toggled_output);
drv_data->output ^= mask;
unlock:
k_mutex_unlock(&drv_data->lock);
return ret;
}
@@ -151,50 +156,83 @@ static DEVICE_API(gpio, gpio_sn74hc595_drv_api_funcs) = {
*/
static int gpio_sn74hc595_init(const struct device *dev)
{
const struct gpio_sn74hc595_config *config = dev->config;
int ret;
struct gpio_sn74hc595_drv_data *drv_data = dev->data;
const struct gpio_sn74hc595_config *config = dev->config;
if (!spi_is_ready_dt(&config->bus)) {
LOG_ERR("SPI bus %s not ready", config->bus.bus->name);
return -ENODEV;
}
if (!gpio_is_ready_dt(&config->reset_gpio)) {
LOG_ERR("GPIO port %s not ready", config->reset_gpio.port->name);
return -ENODEV;
if (config->enable_gpio.port != NULL) {
if (!gpio_is_ready_dt(&config->enable_gpio)) {
LOG_ERR("GPIO port %s not ready", config->enable_gpio.port->name);
return -ENODEV;
}
if (gpio_pin_configure_dt(&config->enable_gpio, GPIO_OUTPUT_INACTIVE) < 0) {
LOG_ERR("Unable to configure ENABLE GPIO pin %u", config->enable_gpio.pin);
return -EINVAL;
}
}
if (gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT_ACTIVE) < 0) {
LOG_ERR("Unable to configure RST GPIO pin %u", config->reset_gpio.pin);
return -EINVAL;
if (config->reset_gpio.port != NULL) {
if (!gpio_is_ready_dt(&config->reset_gpio)) {
LOG_ERR("GPIO port %s not ready", config->reset_gpio.port->name);
return -ENODEV;
}
if (gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT_ACTIVE) < 0) {
LOG_ERR("Unable to configure RST GPIO pin %u", config->reset_gpio.pin);
return -EINVAL;
}
/* The reset signal must be stable for at least 120 ns */
k_busy_wait(1);
gpio_pin_set_dt(&config->reset_gpio, 0);
/* The reset signal must be stable for at least 75 ns before
* clocking the SRCLK pin
*/
k_busy_wait(1);
}
gpio_pin_set(config->reset_gpio.port, config->reset_gpio.pin, 0);
k_mutex_lock(&drv_data->lock, K_FOREVER);
drv_data->output = 0U;
return 0;
/* `drv_data->output` is initialized with the reset_value property from the device tree */
ret = sn74hc595_write(dev, drv_data->output);
k_mutex_unlock(&drv_data->lock);
if (config->enable_gpio.port != NULL) {
gpio_pin_set_dt(&config->enable_gpio, 1);
}
return ret;
}
#define SN74HC595_SPI_OPERATION \
#define SN74HC595_SPI_OPERATION \
((uint16_t)(SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8)))
#define SN74HC595_INIT(n) \
static struct gpio_sn74hc595_drv_data sn74hc595_data_##n = { \
.output = 0, \
.lock = Z_MUTEX_INITIALIZER(sn74hc595_data_##n.lock), \
}; \
\
static const struct gpio_sn74hc595_config sn74hc595_config_##n = { \
.config = { \
.port_pin_mask = \
GPIO_PORT_PIN_MASK_FROM_DT_INST(n), \
}, \
.bus = SPI_DT_SPEC_INST_GET(n, SN74HC595_SPI_OPERATION, 0), \
.reset_gpio = GPIO_DT_SPEC_INST_GET(n, reset_gpios), \
}; \
\
DEVICE_DT_DEFINE(DT_DRV_INST(n), &gpio_sn74hc595_init, NULL, \
&sn74hc595_data_##n, &sn74hc595_config_##n, POST_KERNEL, \
CONFIG_GPIO_SN74HC595_INIT_PRIORITY, &gpio_sn74hc595_drv_api_funcs);
#define SN74HC595_INIT(n) \
static struct gpio_sn74hc595_drv_data sn74hc595_data_##n = { \
.output = DT_INST_PROP(n, reset_value), \
.lock = Z_MUTEX_INITIALIZER(sn74hc595_data_##n.lock), \
}; \
\
static const struct gpio_sn74hc595_config sn74hc595_config_##n = { \
.config = \
{ \
.port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(n), \
}, \
.bus = SPI_DT_SPEC_INST_GET(n, SN74HC595_SPI_OPERATION, 0), \
.reset_gpio = GPIO_DT_SPEC_INST_GET_OR(n, reset_gpio, {0}), \
.enable_gpio = GPIO_DT_SPEC_INST_GET_OR(n, enable_gpios, {0}), \
.num_registers = DT_INST_PROP(n, ngpios) / BITS_PER_BYTE, \
}; \
\
DEVICE_DT_DEFINE(DT_DRV_INST(n), &gpio_sn74hc595_init, NULL, &sn74hc595_data_##n, \
&sn74hc595_config_##n, POST_KERNEL, CONFIG_GPIO_SN74HC595_INIT_PRIORITY, \
&gpio_sn74hc595_drv_api_funcs);
DT_INST_FOREACH_STATUS_OKAY(SN74HC595_INIT)

View File

@@ -1,7 +1,17 @@
# Copyright (c) 2022, Matthias Freese
# SPDX-License-Identifier: Apache-2.0
description: SN74HC595 SPI based GPIO expander
description: |
SN74HC595 SPI based GPIO expander.
The SNx4HC595 is an 8-bit shift register.
Note: There are two clock inputs, one for shifting (SRCLK) and one for storing
(RCLK). On a rising edge, the shift clock puts a bit into the shift register,
but does not make it appear on the output. On a rising edge of the store clock,
all shifted in bits appear on the outputs of the shift register. One can use
`cs-gpios` property of the shift register's parent SPI controller to work the
load clock. If one assigns a GPIO in the GPIO_ACTIVE_LOW configuration, then as
the CS pin goes high at the end of the SPI transaction, the shift register will
load in all shifted bits.
compatible: "ti,sn74hc595"
@@ -10,14 +20,38 @@ include: [gpio-controller.yaml, spi-device.yaml]
properties:
reset-gpios:
type: phandle-array
required: true
description: Reset pin
description: |
An optional GPIO connected to the reset input of the shift register(s).
It will be set to a logical 1 and then held at a logical 0 on init.
enable-gpios:
type: phandle-array
description: |
An optional GPIO connected to the enable input of the shift register(s).
It will be set to a logical 1 on init.
reset-value:
type: int
default: 0x0
description: |
The value to be shifted into the register (or registers) and loaded on
init. This is useful when you want to start with the register(s) in a
specific state and or you don't have control over the hard reset input of
the register(s).
ngpios:
type: int
required: true
const: 8
description: Number of supported gpios
enum:
- 8
- 16
- 24
- 32
description: |
The number of supported gpios. Although each shift register has only eight
outputs, they can be chained together. Due to Zephyr's idea of a GPIO port
only controlling up to 32 pins, the maximum number you can indicate here
is 32, equating to four chained shift registers.
"#gpio-cells":
const: 2