643 lines
15 KiB
C
643 lines
15 KiB
C
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
|
|
/*
|
|
* Copyright (c) 2025 Amlogic, Inc. All rights reserved.
|
|
*/
|
|
|
|
#include <linux/init.h>
|
|
#include <linux/module.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/kobject.h>
|
|
#include <linux/device.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/pm.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/input.h>
|
|
#include <linux/of_platform.h>
|
|
|
|
#define addr_led_reg0 0 //input data
|
|
#define addr_led_reg1 1 //output data
|
|
#define addr_led_reg2 2 //inversion
|
|
#define addr_led_reg3 3 //direction
|
|
|
|
#define MUTE_KEY 0x04
|
|
#define VOLUP_KEY 0x08
|
|
#define VOLDOWN_KEY 0x10
|
|
#define PAULSE_KEY 0x20
|
|
#define RELEASE_KEY 0x00
|
|
|
|
#define DEFAULT_SPEED 230 //default speed 230ms
|
|
|
|
#define LED_DEVICE_NAME "ledring"
|
|
#define LED_CHAR_DEV_NAME "aml-led"
|
|
|
|
#define CMD_LEDRING_ARG 0x100001
|
|
#define CMD_LED_MUTE_ARG 0x100002
|
|
#define CMD_LED_LISTENING_ARG 0x100003
|
|
#define LED_OFF 0
|
|
#define LED_ON 1
|
|
#define MAX_NAME_LEN 50
|
|
|
|
static struct kobject *ledring_kobj;
|
|
static struct i2c_client *g_client;
|
|
|
|
static int m_temp = -1;
|
|
static int m_index;
|
|
static int timeout_flag;
|
|
static int timeout;
|
|
|
|
static struct _key_led {
|
|
struct input_dev *pca_input_dev;
|
|
char dev_name[MAX_NAME_LEN];
|
|
struct timer_list mtimer;
|
|
struct work_struct list_work;
|
|
int run_time;
|
|
int key_num;
|
|
int key_tmp_val;
|
|
int key_last_val;
|
|
int action_times;
|
|
struct class *cls;
|
|
int major;
|
|
int mode;
|
|
} *key_led_des;
|
|
|
|
static struct _key_des {
|
|
char name[MAX_NAME_LEN];
|
|
unsigned int key_val;
|
|
int pin;
|
|
int irq;
|
|
} *exp_key;
|
|
|
|
static struct _style {
|
|
int num; /*the number of led*/
|
|
int times; /*1: stop after action once, 0: cycle action*/
|
|
int speed; /*speed. N(ms)*/
|
|
int timeout; /*timeout N(s)*/
|
|
int style[12][12]; /*style data*/
|
|
} style_data = {
|
|
6, 0, DEFAULT_SPEED, 0,
|
|
{
|
|
/*led1 led2 led3 led4 led5 led6*/
|
|
{0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, /*state1*/
|
|
{0x00, 0x00, 0x00, 0x00, 0x01, 0x00}, /*state2*/
|
|
{0x00, 0x00, 0x00, 0x01, 0x00, 0x00}, /*state3*/
|
|
{0x00, 0x00, 0x01, 0x00, 0x00, 0x00}, /*state4*/
|
|
{0x00, 0x01, 0x00, 0x00, 0x00, 0x00}, /*state5*/
|
|
{0x01, 0x00, 0x00, 0x00, 0x00, 0x00}, /*state6*/
|
|
},
|
|
};
|
|
|
|
static int leds_light(void);
|
|
static int leds_init(int mode);
|
|
|
|
static const struct i2c_device_id ledring_id[] = {
|
|
{"aml_pca9557"},
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(i2c, ledring_id);
|
|
|
|
static const struct of_device_id ledring_dt_ids[] = {
|
|
{
|
|
.compatible = "aml, ledring",
|
|
.data = (void *)NULL
|
|
},
|
|
{},
|
|
};
|
|
MODULE_DEVICE_TABLE(of, ledring_dt_ids);
|
|
|
|
static ssize_t speed_level_write(struct kobject *kobj,
|
|
struct kobj_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
int ret;
|
|
int m_speed;
|
|
|
|
ret = kstrtouint(buf, 10, &m_speed);
|
|
if (ret == 0) {
|
|
pr_info("set speed level: %d ms\n", m_speed);
|
|
} else {
|
|
pr_info("set speed level fail, use default speed!!\n");
|
|
m_speed = DEFAULT_SPEED;
|
|
}
|
|
key_led_des->run_time = m_speed;
|
|
schedule_work(&key_led_des->list_work);
|
|
mod_timer(&key_led_des->mtimer,
|
|
jiffies + key_led_des->run_time * HZ / 1000);
|
|
return count;
|
|
}
|
|
|
|
static ssize_t run_state_read(struct kobject *kobj,
|
|
struct kobj_attribute *attr,
|
|
char *buf)
|
|
{
|
|
pr_info("close ledring: echo 0 > runflag\n");
|
|
pr_info("open ledring: echo 1 > runflag\n");
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t run_state_write(struct kobject *kobj,
|
|
struct kobj_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
int ret;
|
|
int runflag;
|
|
|
|
ret = kstrtouint(buf, 10, &runflag);
|
|
if (ret == 0) {
|
|
if (runflag == 0) {
|
|
del_timer(&key_led_des->mtimer);
|
|
pr_info("stop running ledring.\n");
|
|
} else if (runflag == 1) {
|
|
pr_info("start running ledring.\n");
|
|
schedule_work(&key_led_des->list_work);
|
|
mod_timer(&key_led_des->mtimer,
|
|
jiffies + key_led_des->run_time * HZ / 1000);
|
|
}
|
|
} else {
|
|
pr_info("set run state fail!\n");
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static ssize_t speed_level_read(struct kobject *kobj,
|
|
struct kobj_attribute *attr,
|
|
char *buf)
|
|
{
|
|
pr_info("current speed level: %d ms\n", key_led_des->run_time);
|
|
return 0;
|
|
}
|
|
|
|
static struct kobj_attribute
|
|
running_attribute =
|
|
__ATTR(runflag, 0664,
|
|
run_state_read,
|
|
run_state_write);
|
|
static struct kobj_attribute
|
|
speed_attribute =
|
|
__ATTR(speed, 0664,
|
|
speed_level_read,
|
|
speed_level_write);
|
|
static struct attribute
|
|
*attrs[] = {
|
|
&speed_attribute.attr,
|
|
&running_attribute.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group
|
|
attr_group = {
|
|
.attrs = attrs,
|
|
};
|
|
|
|
static void mtimer_function(struct timer_list *timer)
|
|
{
|
|
schedule_work(&key_led_des->list_work);
|
|
mod_timer(&key_led_des->mtimer,
|
|
jiffies + key_led_des->run_time * HZ / 1000);
|
|
}
|
|
|
|
static void list_work_func(struct work_struct *work)
|
|
{
|
|
int ret;
|
|
int key_val = 0;
|
|
|
|
if (key_led_des->mode == 0) {
|
|
if (timeout_flag == 0) {
|
|
leds_light();
|
|
} else {
|
|
timeout--;
|
|
if (timeout <= 0) {
|
|
timeout = 0;
|
|
del_timer(&key_led_des->mtimer);
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, 0);
|
|
if (ret < 0)
|
|
pr_err("led set reg1 fail!\n");
|
|
} else {
|
|
leds_light();
|
|
}
|
|
}
|
|
} else if (key_led_des->mode == 1) {
|
|
key_val = i2c_smbus_read_byte_data(g_client, addr_led_reg0) & 0x3C;
|
|
if (key_led_des->key_tmp_val != key_val) {
|
|
key_led_des->key_tmp_val = key_val;
|
|
if (key_val != 0)
|
|
key_led_des->key_last_val = key_val;
|
|
switch (key_led_des->key_last_val) {
|
|
case MUTE_KEY:
|
|
if (key_val != 0) {
|
|
pr_info("key \"%s\" down\n", exp_key[0].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[0].key_val, 1);
|
|
} else {
|
|
pr_info("key \"%s\" up\n", exp_key[0].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[0].key_val, 0);
|
|
}
|
|
input_event(key_led_des->pca_input_dev, EV_SYN, 0, 0);
|
|
break;
|
|
case VOLUP_KEY:
|
|
if (key_val != 0) {
|
|
pr_info("key \"%s\" down\n", exp_key[2].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[2].key_val, 1);
|
|
} else {
|
|
pr_info("key \"%s\" up\n", exp_key[2].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[2].key_val, 0);
|
|
}
|
|
input_event(key_led_des->pca_input_dev, EV_SYN, 0, 0);
|
|
break;
|
|
case VOLDOWN_KEY:
|
|
if (key_val != 0) {
|
|
pr_info("key \"%s\" down\n", exp_key[3].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[3].key_val, 1);
|
|
} else {
|
|
pr_info("key \"%s\" up\n", exp_key[3].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[3].key_val, 0);
|
|
}
|
|
input_event(key_led_des->pca_input_dev, EV_SYN, 0, 0);
|
|
break;
|
|
case PAULSE_KEY:
|
|
if (key_val != 0) {
|
|
pr_info("key \"%s\" down\n", exp_key[1].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[1].key_val, 1);
|
|
} else {
|
|
pr_info("key \"%s\" up\n", exp_key[1].name);
|
|
input_event(key_led_des->pca_input_dev, EV_KEY,
|
|
exp_key[1].key_val, 0);
|
|
}
|
|
input_event(key_led_des->pca_input_dev, EV_SYN, 0, 0);
|
|
break;
|
|
default:
|
|
pr_info("invalid key, please check!\n");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int setup_timer_task(int mode)
|
|
{
|
|
timer_setup(&key_led_des->mtimer, mtimer_function, mode);
|
|
mod_timer(&key_led_des->mtimer,
|
|
jiffies + key_led_des->run_time * HZ / 1000);
|
|
INIT_WORK(&key_led_des->list_work, list_work_func);
|
|
return 0;
|
|
}
|
|
|
|
static int leds_init(int mode)
|
|
{
|
|
int ret;
|
|
|
|
if (mode == 0) {
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg3, 0);
|
|
if (ret < 0) {
|
|
pr_err("led init reg3 fail!\n");
|
|
return -1;
|
|
}
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, 0);
|
|
if (ret < 0) {
|
|
pr_err("led init reg1 fail!\n");
|
|
return -1;
|
|
}
|
|
} else if (mode == 1) {
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg3, 0x3F);
|
|
if (ret < 0) {
|
|
pr_err("led init reg3 fail!\n");
|
|
return -1;
|
|
}
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, 0xC0);
|
|
if (ret < 0) {
|
|
pr_err("led init reg1 fail!\n");
|
|
return -1;
|
|
}
|
|
} else {
|
|
pr_info("%s fail,%d mode is not support!\n", __func__, mode);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int leds_light(void)
|
|
{
|
|
int ret = 0;
|
|
int i = 0;
|
|
int temp = 0;
|
|
|
|
for (i = 0; i < style_data.num; i++) {
|
|
temp |= style_data.style[m_index][i]
|
|
<< (style_data.num - i + 1);
|
|
}
|
|
if (m_temp != temp) {
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, temp);
|
|
if (ret < 0)
|
|
pr_err("led lit fail!\n");
|
|
m_temp = temp;
|
|
}
|
|
if (++m_index > style_data.num - 1) {
|
|
if (style_data.times != 0) {
|
|
if (--key_led_des->action_times <= 0)
|
|
del_timer(&key_led_des->mtimer);
|
|
}
|
|
m_index = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static long leds_ioctl(struct file *file,
|
|
unsigned int cmd,
|
|
unsigned long args)
|
|
{
|
|
int ret, val;
|
|
|
|
switch (cmd) {
|
|
case CMD_LEDRING_ARG:
|
|
m_index = 0;
|
|
del_timer(&key_led_des->mtimer);
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, 0);
|
|
if (ret < 0)
|
|
pr_err("led set reg1 fail!\n");
|
|
ret = copy_from_user(&style_data,
|
|
(int *)args, sizeof(style_data));
|
|
if (style_data.speed < 0) {
|
|
pr_info("set speed level fail,use default speed!!\n");
|
|
key_led_des->run_time = DEFAULT_SPEED;
|
|
} else if (key_led_des->run_time != style_data.speed) {
|
|
key_led_des->run_time = style_data.speed;
|
|
}
|
|
if (style_data.timeout != 0) {
|
|
timeout_flag = 1;
|
|
timeout = style_data.timeout * 1000 / key_led_des->run_time;
|
|
} else {
|
|
timeout_flag = 0;
|
|
}
|
|
if (style_data.times < 0)
|
|
style_data.times = 0;
|
|
key_led_des->action_times = style_data.times;
|
|
schedule_work(&key_led_des->list_work);
|
|
mod_timer(&key_led_des->mtimer,
|
|
jiffies + key_led_des->run_time * HZ / 1000);
|
|
break;
|
|
case CMD_LED_MUTE_ARG:
|
|
val = i2c_smbus_read_byte_data(g_client, addr_led_reg1);
|
|
if (args == LED_OFF) {
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, (1 << 7) | val);
|
|
if (ret < 0)
|
|
pr_err("led init reg1 fail!\n");
|
|
} else if (args == LED_ON) {
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, ~(1 << 7) & val);
|
|
if (ret < 0)
|
|
pr_err("led init reg1 fail!\n");
|
|
}
|
|
break;
|
|
case CMD_LED_LISTENING_ARG:
|
|
val = i2c_smbus_read_byte_data(g_client, addr_led_reg1);
|
|
if (args == LED_OFF) {
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, (1 << 6) | val);
|
|
if (ret < 0)
|
|
pr_err("led init reg1 fail!\n");
|
|
} else if (args == LED_ON) {
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, ~(1 << 6) & val);
|
|
if (ret < 0)
|
|
pr_err("led init reg1 fail!\n");
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t leds_read(struct file *filp, char __user *buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
unsigned long ret;
|
|
|
|
ret = copy_to_user(buf, &key_led_des->mode,
|
|
sizeof(key_led_des->mode));
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations led_fops = {
|
|
.owner = THIS_MODULE,
|
|
.read = leds_read,
|
|
.compat_ioctl = leds_ioctl,
|
|
.unlocked_ioctl = leds_ioctl,
|
|
};
|
|
|
|
static int ledring_parse_child_dt(const struct device *dev, int *mode)
|
|
{
|
|
int ret, cnt;
|
|
int m_mode = 0;
|
|
const char *uname;
|
|
|
|
if (dev->of_node) {
|
|
ret = of_property_read_u32(dev->of_node, "mode", &m_mode);
|
|
if (ret) {
|
|
pr_err("does not have a valid mode property\n");
|
|
return -EINVAL;
|
|
}
|
|
*mode = m_mode;
|
|
ret = of_property_read_string(dev->of_node, "key_dev_name", &uname);
|
|
if (ret) {
|
|
pr_err("does not have a valid key_dev_name property\n");
|
|
return -EINVAL;
|
|
}
|
|
strncpy(key_led_des->dev_name, uname, MAX_NAME_LEN);
|
|
if (m_mode == 1) {
|
|
ret = of_property_read_u32(dev->of_node, "key_num",
|
|
&key_led_des->key_num);
|
|
if (ret) {
|
|
pr_err("failed to get key_num!\n");
|
|
return -EINVAL;
|
|
}
|
|
exp_key = kzalloc(sizeof(*exp_key) * key_led_des->key_num,
|
|
GFP_KERNEL);
|
|
if (!exp_key)
|
|
return -ENOMEM;
|
|
for (cnt = 0; cnt < key_led_des->key_num; cnt++) {
|
|
ret = of_property_read_string_index(dev->of_node,
|
|
"key_name", cnt, &uname);
|
|
if (ret < 0) {
|
|
pr_err("invalid key name index[%d]\n", cnt);
|
|
return -EINVAL;
|
|
}
|
|
strncpy(exp_key[cnt].name, uname, MAX_NAME_LEN);
|
|
ret = of_property_read_u32_index(dev->of_node,
|
|
"key_value", cnt, &exp_key[cnt].key_val);
|
|
if (ret < 0) {
|
|
pr_err("invalid key code index[%d]\n", cnt);
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int ledring_probe(struct i2c_client *client)
|
|
{
|
|
int ret, i;
|
|
int try_times = 0;
|
|
struct device *dev = &client->dev;
|
|
|
|
g_client = client;
|
|
pr_info("%s\n", __func__);
|
|
key_led_des = devm_kzalloc(dev, sizeof(struct _key_led), GFP_KERNEL);
|
|
if (!key_led_des)
|
|
return -ENOMEM;
|
|
|
|
ret = ledring_parse_child_dt(dev, &key_led_des->mode);
|
|
if (ret < 0) {
|
|
pr_err("%s,ledring_parse_child_dt fail!\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (key_led_des->mode == 0) {
|
|
ledring_kobj = kobject_create_and_add("ledring", kernel_kobj);
|
|
if (!ledring_kobj)
|
|
return -ENOMEM;
|
|
ret = sysfs_create_group(ledring_kobj, &attr_group);
|
|
if (ret)
|
|
goto err1;
|
|
key_led_des->run_time = DEFAULT_SPEED;
|
|
for (i = 0; i < 3; i++) {
|
|
ret = leds_init(key_led_des->mode);
|
|
if (ret != 0) {
|
|
if (++try_times >= 3)
|
|
goto err1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for (i = 0; i < 3; i++) {
|
|
ret = leds_init(key_led_des->mode);
|
|
if (ret != 0) {
|
|
if (++try_times >= 3)
|
|
goto err3;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
key_led_des->pca_input_dev = input_allocate_device();
|
|
if (!key_led_des->pca_input_dev) {
|
|
pr_err("input_allocate_device err!\n");
|
|
goto err3;
|
|
}
|
|
set_bit(EV_SYN, key_led_des->pca_input_dev->evbit);
|
|
set_bit(EV_KEY, key_led_des->pca_input_dev->evbit);
|
|
for (i = 0; i < key_led_des->key_num; i++)
|
|
set_bit(exp_key[i].key_val, key_led_des->pca_input_dev->keybit);
|
|
|
|
key_led_des->pca_input_dev->name = key_led_des->dev_name;
|
|
ret = input_register_device(key_led_des->pca_input_dev);
|
|
if (ret != 0) {
|
|
pr_err("input_register_device err!\n");
|
|
goto err2;
|
|
}
|
|
key_led_des->run_time = 100; //200ms
|
|
key_led_des->key_tmp_val = 0;
|
|
}
|
|
setup_timer_task(key_led_des->mode);
|
|
key_led_des->major = register_chrdev(0, LED_CHAR_DEV_NAME, &led_fops);
|
|
key_led_des->cls = class_create(LED_DEVICE_NAME);
|
|
device_create(key_led_des->cls, NULL,
|
|
MKDEV(key_led_des->major, 0), NULL, LED_DEVICE_NAME);
|
|
|
|
return 0;
|
|
err1:
|
|
kobject_put(ledring_kobj);
|
|
return -ENOMEM;
|
|
err2:
|
|
input_free_device(key_led_des->pca_input_dev);
|
|
return -ENOMEM;
|
|
err3:
|
|
return -ENOMEM;
|
|
}
|
|
|
|
static void ledring_remove(struct i2c_client *client)
|
|
{
|
|
pr_info("%s\n", __func__);
|
|
|
|
device_destroy(key_led_des->cls, MKDEV(key_led_des->major, 0));
|
|
class_destroy(key_led_des->cls);
|
|
unregister_chrdev(key_led_des->major, LED_CHAR_DEV_NAME);
|
|
flush_work(&key_led_des->list_work);
|
|
del_timer(&key_led_des->mtimer);
|
|
|
|
if (key_led_des->mode == 0) {
|
|
kobject_put(ledring_kobj);
|
|
} else {
|
|
input_unregister_device(key_led_des->pca_input_dev);
|
|
input_free_device(key_led_des->pca_input_dev);
|
|
}
|
|
}
|
|
|
|
static int ledring_resume(struct device *dev)
|
|
{
|
|
pr_info("enter %s\n", __func__);
|
|
|
|
schedule_work(&key_led_des->list_work);
|
|
mod_timer(&key_led_des->mtimer,
|
|
jiffies + key_led_des->run_time * HZ / 1000);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ledring_suspend(struct device *dev)
|
|
{
|
|
int ret;
|
|
|
|
pr_info("enter %s\n", __func__);
|
|
del_timer(&key_led_des->mtimer);
|
|
ret = i2c_smbus_write_byte_data(g_client,
|
|
addr_led_reg1, 0);
|
|
if (ret < 0)
|
|
pr_err("led set reg1 fail!\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct dev_pm_ops ledring_pm = {
|
|
.suspend = ledring_suspend,
|
|
.resume = ledring_resume,
|
|
};
|
|
|
|
static struct i2c_driver ledring_drv = {
|
|
.driver = {
|
|
.name = "aml_ledring",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = ledring_dt_ids,
|
|
.pm = &ledring_pm,
|
|
},
|
|
.probe = ledring_probe,
|
|
.remove = ledring_remove,
|
|
.id_table = ledring_id,
|
|
};
|
|
|
|
int aml_pca9557_init(void)
|
|
{
|
|
return i2c_add_driver(&ledring_drv);
|
|
}
|
|
|
|
void __exit aml_pca9557_exit(void)
|
|
{
|
|
i2c_del_driver(&ledring_drv);
|
|
}
|