| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Apple Motion Sensor driver (I2C variant) |
| * |
| * Copyright (C) 2005 Stelian Pop (stelian@popies.net) |
| * Copyright (C) 2006 Michael Hanselmann (linux-kernel@hansmi.ch) |
| * |
| * Clean room implementation based on the reverse engineered Mac OS X driver by |
| * Johannes Berg <johannes@sipsolutions.net>, documentation available at |
| * http://johannes.sipsolutions.net/PowerBook/Apple_Motion_Sensor_Specification |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/types.h> |
| #include <linux/errno.h> |
| #include <linux/init.h> |
| #include <linux/delay.h> |
| |
| #include "ams.h" |
| |
| /* AMS registers */ |
| #define AMS_COMMAND 0x00 /* command register */ |
| #define AMS_STATUS 0x01 /* status register */ |
| #define AMS_CTRL1 0x02 /* read control 1 (number of values) */ |
| #define AMS_CTRL2 0x03 /* read control 2 (offset?) */ |
| #define AMS_CTRL3 0x04 /* read control 3 (size of each value?) */ |
| #define AMS_DATA1 0x05 /* read data 1 */ |
| #define AMS_DATA2 0x06 /* read data 2 */ |
| #define AMS_DATA3 0x07 /* read data 3 */ |
| #define AMS_DATA4 0x08 /* read data 4 */ |
| #define AMS_DATAX 0x20 /* data X */ |
| #define AMS_DATAY 0x21 /* data Y */ |
| #define AMS_DATAZ 0x22 /* data Z */ |
| #define AMS_FREEFALL 0x24 /* freefall int control */ |
| #define AMS_SHOCK 0x25 /* shock int control */ |
| #define AMS_SENSLOW 0x26 /* sensitivity low limit */ |
| #define AMS_SENSHIGH 0x27 /* sensitivity high limit */ |
| #define AMS_CTRLX 0x28 /* control X */ |
| #define AMS_CTRLY 0x29 /* control Y */ |
| #define AMS_CTRLZ 0x2A /* control Z */ |
| #define AMS_UNKNOWN1 0x2B /* unknown 1 */ |
| #define AMS_UNKNOWN2 0x2C /* unknown 2 */ |
| #define AMS_UNKNOWN3 0x2D /* unknown 3 */ |
| #define AMS_VENDOR 0x2E /* vendor */ |
| |
| /* AMS commands - use with the AMS_COMMAND register */ |
| enum ams_i2c_cmd { |
| AMS_CMD_NOOP = 0, |
| AMS_CMD_VERSION, |
| AMS_CMD_READMEM, |
| AMS_CMD_WRITEMEM, |
| AMS_CMD_ERASEMEM, |
| AMS_CMD_READEE, |
| AMS_CMD_WRITEEE, |
| AMS_CMD_RESET, |
| AMS_CMD_START, |
| }; |
| |
| static int ams_i2c_probe(struct i2c_client *client); |
| static void ams_i2c_remove(struct i2c_client *client); |
| |
| static const struct i2c_device_id ams_id[] = { |
| { "MAC,accelerometer_1" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(i2c, ams_id); |
| |
| static struct i2c_driver ams_i2c_driver = { |
| .driver = { |
| .name = "ams", |
| }, |
| .probe = ams_i2c_probe, |
| .remove = ams_i2c_remove, |
| .id_table = ams_id, |
| }; |
| |
| static s32 ams_i2c_read(u8 reg) |
| { |
| return i2c_smbus_read_byte_data(ams_info.i2c_client, reg); |
| } |
| |
| static int ams_i2c_write(u8 reg, u8 value) |
| { |
| return i2c_smbus_write_byte_data(ams_info.i2c_client, reg, value); |
| } |
| |
| static int ams_i2c_cmd(enum ams_i2c_cmd cmd) |
| { |
| s32 result; |
| int count = 3; |
| |
| ams_i2c_write(AMS_COMMAND, cmd); |
| msleep(5); |
| |
| while (count--) { |
| result = ams_i2c_read(AMS_COMMAND); |
| if (result == 0 || result & 0x80) |
| return 0; |
| |
| schedule_timeout_uninterruptible(HZ / 20); |
| } |
| |
| return -1; |
| } |
| |
| static void ams_i2c_set_irq(enum ams_irq reg, char enable) |
| { |
| if (reg & AMS_IRQ_FREEFALL) { |
| u8 val = ams_i2c_read(AMS_CTRLX); |
| if (enable) |
| val |= 0x80; |
| else |
| val &= ~0x80; |
| ams_i2c_write(AMS_CTRLX, val); |
| } |
| |
| if (reg & AMS_IRQ_SHOCK) { |
| u8 val = ams_i2c_read(AMS_CTRLY); |
| if (enable) |
| val |= 0x80; |
| else |
| val &= ~0x80; |
| ams_i2c_write(AMS_CTRLY, val); |
| } |
| |
| if (reg & AMS_IRQ_GLOBAL) { |
| u8 val = ams_i2c_read(AMS_CTRLZ); |
| if (enable) |
| val |= 0x80; |
| else |
| val &= ~0x80; |
| ams_i2c_write(AMS_CTRLZ, val); |
| } |
| } |
| |
| static void ams_i2c_clear_irq(enum ams_irq reg) |
| { |
| if (reg & AMS_IRQ_FREEFALL) |
| ams_i2c_write(AMS_FREEFALL, 0); |
| |
| if (reg & AMS_IRQ_SHOCK) |
| ams_i2c_write(AMS_SHOCK, 0); |
| } |
| |
| static u8 ams_i2c_get_vendor(void) |
| { |
| return ams_i2c_read(AMS_VENDOR); |
| } |
| |
| static void ams_i2c_get_xyz(s8 *x, s8 *y, s8 *z) |
| { |
| *x = ams_i2c_read(AMS_DATAX); |
| *y = ams_i2c_read(AMS_DATAY); |
| *z = ams_i2c_read(AMS_DATAZ); |
| } |
| |
| static int ams_i2c_probe(struct i2c_client *client) |
| { |
| int vmaj, vmin; |
| int result; |
| |
| /* There can be only one */ |
| if (unlikely(ams_info.has_device)) |
| return -ENODEV; |
| |
| ams_info.i2c_client = client; |
| |
| if (ams_i2c_cmd(AMS_CMD_RESET)) { |
| printk(KERN_INFO "ams: Failed to reset the device\n"); |
| return -ENODEV; |
| } |
| |
| if (ams_i2c_cmd(AMS_CMD_START)) { |
| printk(KERN_INFO "ams: Failed to start the device\n"); |
| return -ENODEV; |
| } |
| |
| /* get version/vendor information */ |
| ams_i2c_write(AMS_CTRL1, 0x02); |
| ams_i2c_write(AMS_CTRL2, 0x85); |
| ams_i2c_write(AMS_CTRL3, 0x01); |
| |
| ams_i2c_cmd(AMS_CMD_READMEM); |
| |
| vmaj = ams_i2c_read(AMS_DATA1); |
| vmin = ams_i2c_read(AMS_DATA2); |
| if (vmaj != 1 || vmin != 52) { |
| printk(KERN_INFO "ams: Incorrect device version (%d.%d)\n", |
| vmaj, vmin); |
| return -ENODEV; |
| } |
| |
| ams_i2c_cmd(AMS_CMD_VERSION); |
| |
| vmaj = ams_i2c_read(AMS_DATA1); |
| vmin = ams_i2c_read(AMS_DATA2); |
| if (vmaj != 0 || vmin != 1) { |
| printk(KERN_INFO "ams: Incorrect firmware version (%d.%d)\n", |
| vmaj, vmin); |
| return -ENODEV; |
| } |
| |
| /* Disable interrupts */ |
| ams_i2c_set_irq(AMS_IRQ_ALL, 0); |
| |
| result = ams_sensor_attach(); |
| if (result < 0) |
| return result; |
| |
| /* Set default values */ |
| ams_i2c_write(AMS_SENSLOW, 0x15); |
| ams_i2c_write(AMS_SENSHIGH, 0x60); |
| ams_i2c_write(AMS_CTRLX, 0x08); |
| ams_i2c_write(AMS_CTRLY, 0x0F); |
| ams_i2c_write(AMS_CTRLZ, 0x4F); |
| ams_i2c_write(AMS_UNKNOWN1, 0x14); |
| |
| /* Clear interrupts */ |
| ams_i2c_clear_irq(AMS_IRQ_ALL); |
| |
| ams_info.has_device = 1; |
| |
| /* Enable interrupts */ |
| ams_i2c_set_irq(AMS_IRQ_ALL, 1); |
| |
| printk(KERN_INFO "ams: Found I2C based motion sensor\n"); |
| |
| return 0; |
| } |
| |
| static void ams_i2c_remove(struct i2c_client *client) |
| { |
| if (ams_info.has_device) { |
| ams_sensor_detach(); |
| |
| /* Disable interrupts */ |
| ams_i2c_set_irq(AMS_IRQ_ALL, 0); |
| |
| /* Clear interrupts */ |
| ams_i2c_clear_irq(AMS_IRQ_ALL); |
| |
| printk(KERN_INFO "ams: Unloading\n"); |
| |
| ams_info.has_device = 0; |
| } |
| } |
| |
| static void ams_i2c_exit(void) |
| { |
| i2c_del_driver(&ams_i2c_driver); |
| } |
| |
| int __init ams_i2c_init(struct device_node *np) |
| { |
| /* Set implementation stuff */ |
| ams_info.of_node = np; |
| ams_info.exit = ams_i2c_exit; |
| ams_info.get_vendor = ams_i2c_get_vendor; |
| ams_info.get_xyz = ams_i2c_get_xyz; |
| ams_info.clear_irq = ams_i2c_clear_irq; |
| ams_info.bustype = BUS_I2C; |
| |
| return i2c_add_driver(&ams_i2c_driver); |
| } |