forked from mirrors/linux
		
	thunderbolt: Add suspend/hibernate support
We use _noirq since we have to restore the pci tunnels before the pci core wakes the tunneled devices. Signed-off-by: Andreas Noever <andreas.noever@gmail.com> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
		
							parent
							
								
									c90553b3c4
								
							
						
					
					
						commit
						23dd5bb49d
					
				
					 4 changed files with 183 additions and 0 deletions
				
			
		|  | @ -7,6 +7,7 @@ | ||||||
|  * Copyright (c) 2014 Andreas Noever <andreas.noever@gmail.com> |  * Copyright (c) 2014 Andreas Noever <andreas.noever@gmail.com> | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | #include <linux/pm_runtime.h> | ||||||
| #include <linux/slab.h> | #include <linux/slab.h> | ||||||
| #include <linux/errno.h> | #include <linux/errno.h> | ||||||
| #include <linux/pci.h> | #include <linux/pci.h> | ||||||
|  | @ -492,6 +493,22 @@ static irqreturn_t nhi_msi(int irq, void *data) | ||||||
| 	return IRQ_HANDLED; | 	return IRQ_HANDLED; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static int nhi_suspend_noirq(struct device *dev) | ||||||
|  | { | ||||||
|  | 	struct pci_dev *pdev = to_pci_dev(dev); | ||||||
|  | 	struct tb *tb = pci_get_drvdata(pdev); | ||||||
|  | 	thunderbolt_suspend(tb); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static int nhi_resume_noirq(struct device *dev) | ||||||
|  | { | ||||||
|  | 	struct pci_dev *pdev = to_pci_dev(dev); | ||||||
|  | 	struct tb *tb = pci_get_drvdata(pdev); | ||||||
|  | 	thunderbolt_resume(tb); | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static void nhi_shutdown(struct tb_nhi *nhi) | static void nhi_shutdown(struct tb_nhi *nhi) | ||||||
| { | { | ||||||
| 	int i; | 	int i; | ||||||
|  | @ -600,6 +617,21 @@ static void nhi_remove(struct pci_dev *pdev) | ||||||
| 	nhi_shutdown(nhi); | 	nhi_shutdown(nhi); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /*
 | ||||||
|  |  * The tunneled pci bridges are siblings of us. Use resume_noirq to reenable | ||||||
|  |  * the tunnels asap. A corresponding pci quirk blocks the downstream bridges | ||||||
|  |  * resume_noirq until we are done. | ||||||
|  |  */ | ||||||
|  | static const struct dev_pm_ops nhi_pm_ops = { | ||||||
|  | 	.suspend_noirq = nhi_suspend_noirq, | ||||||
|  | 	.resume_noirq = nhi_resume_noirq, | ||||||
|  | 	.freeze_noirq = nhi_suspend_noirq, /*
 | ||||||
|  | 					    * we just disable hotplug, the | ||||||
|  | 					    * pci-tunnels stay alive. | ||||||
|  | 					    */ | ||||||
|  | 	.restore_noirq = nhi_resume_noirq, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| struct pci_device_id nhi_ids[] = { | struct pci_device_id nhi_ids[] = { | ||||||
| 	/*
 | 	/*
 | ||||||
| 	 * We have to specify class, the TB bridges use the same device and | 	 * We have to specify class, the TB bridges use the same device and | ||||||
|  | @ -626,6 +658,7 @@ static struct pci_driver nhi_driver = { | ||||||
| 	.id_table = nhi_ids, | 	.id_table = nhi_ids, | ||||||
| 	.probe = nhi_probe, | 	.probe = nhi_probe, | ||||||
| 	.remove = nhi_remove, | 	.remove = nhi_remove, | ||||||
|  | 	.driver.pm = &nhi_pm_ops, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| static int __init nhi_init(void) | static int __init nhi_init(void) | ||||||
|  |  | ||||||
|  | @ -229,6 +229,30 @@ static void tb_dump_switch(struct tb *tb, struct tb_regs_switch_header *sw) | ||||||
| 		sw->__unknown1, sw->__unknown4); | 		sw->__unknown1, sw->__unknown4); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /**
 | ||||||
|  |  * reset_switch() - reconfigure route, enable and send TB_CFG_PKG_RESET | ||||||
|  |  * | ||||||
|  |  * Return: Returns 0 on success or an error code on failure. | ||||||
|  |  */ | ||||||
|  | int tb_switch_reset(struct tb *tb, u64 route) | ||||||
|  | { | ||||||
|  | 	struct tb_cfg_result res; | ||||||
|  | 	struct tb_regs_switch_header header = { | ||||||
|  | 		header.route_hi = route >> 32, | ||||||
|  | 		header.route_lo = route, | ||||||
|  | 		header.enabled = true, | ||||||
|  | 	}; | ||||||
|  | 	tb_info(tb, "resetting switch at %llx\n", route); | ||||||
|  | 	res.err = tb_cfg_write(tb->ctl, ((u32 *) &header) + 2, route, | ||||||
|  | 			0, 2, 2, 2); | ||||||
|  | 	if (res.err) | ||||||
|  | 		return res.err; | ||||||
|  | 	res = tb_cfg_reset(tb->ctl, route, TB_CFG_DEFAULT_TIMEOUT); | ||||||
|  | 	if (res.err > 0) | ||||||
|  | 		return -EIO; | ||||||
|  | 	return res.err; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| struct tb_switch *get_switch_at_route(struct tb_switch *sw, u64 route) | struct tb_switch *get_switch_at_route(struct tb_switch *sw, u64 route) | ||||||
| { | { | ||||||
| 	u8 next_port = route; /*
 | 	u8 next_port = route; /*
 | ||||||
|  | @ -412,3 +436,63 @@ void tb_sw_set_unpplugged(struct tb_switch *sw) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | int tb_switch_resume(struct tb_switch *sw) | ||||||
|  | { | ||||||
|  | 	int i, err; | ||||||
|  | 	u64 uid; | ||||||
|  | 	tb_sw_info(sw, "resuming switch\n"); | ||||||
|  | 
 | ||||||
|  | 	err = tb_eeprom_read_uid(sw, &uid); | ||||||
|  | 	if (err) { | ||||||
|  | 		tb_sw_warn(sw, "uid read failed\n"); | ||||||
|  | 		return err; | ||||||
|  | 	} | ||||||
|  | 	if (sw->uid != uid) { | ||||||
|  | 		tb_sw_info(sw, | ||||||
|  | 			"changed while suspended (uid %#llx -> %#llx)\n", | ||||||
|  | 			sw->uid, uid); | ||||||
|  | 		return -ENODEV; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/* upload configuration */ | ||||||
|  | 	err = tb_sw_write(sw, 1 + (u32 *) &sw->config, TB_CFG_SWITCH, 1, 3); | ||||||
|  | 	if (err) | ||||||
|  | 		return err; | ||||||
|  | 
 | ||||||
|  | 	err = tb_plug_events_active(sw, true); | ||||||
|  | 	if (err) | ||||||
|  | 		return err; | ||||||
|  | 
 | ||||||
|  | 	/* check for surviving downstream switches */ | ||||||
|  | 	for (i = 1; i <= sw->config.max_port_number; i++) { | ||||||
|  | 		struct tb_port *port = &sw->ports[i]; | ||||||
|  | 		if (tb_is_upstream_port(port)) | ||||||
|  | 			continue; | ||||||
|  | 		if (!port->remote) | ||||||
|  | 			continue; | ||||||
|  | 		if (tb_wait_for_port(port, true) <= 0 | ||||||
|  | 			|| tb_switch_resume(port->remote->sw)) { | ||||||
|  | 			tb_port_warn(port, | ||||||
|  | 				     "lost during suspend, disconnecting\n"); | ||||||
|  | 			tb_sw_set_unpplugged(port->remote->sw); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void tb_switch_suspend(struct tb_switch *sw) | ||||||
|  | { | ||||||
|  | 	int i, err; | ||||||
|  | 	err = tb_plug_events_active(sw, false); | ||||||
|  | 	if (err) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	for (i = 1; i <= sw->config.max_port_number; i++) { | ||||||
|  | 		if (!tb_is_upstream_port(&sw->ports[i]) && sw->ports[i].remote) | ||||||
|  | 			tb_switch_suspend(sw->ports[i].remote->sw); | ||||||
|  | 	} | ||||||
|  | 	/*
 | ||||||
|  | 	 * TODO: invoke tb_cfg_prepare_to_sleep here? does not seem to have any | ||||||
|  | 	 * effect? | ||||||
|  | 	 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -68,6 +68,28 @@ static void tb_free_invalid_tunnels(struct tb *tb) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /**
 | ||||||
|  |  * tb_free_unplugged_children() - traverse hierarchy and free unplugged switches | ||||||
|  |  */ | ||||||
|  | static void tb_free_unplugged_children(struct tb_switch *sw) | ||||||
|  | { | ||||||
|  | 	int i; | ||||||
|  | 	for (i = 1; i <= sw->config.max_port_number; i++) { | ||||||
|  | 		struct tb_port *port = &sw->ports[i]; | ||||||
|  | 		if (tb_is_upstream_port(port)) | ||||||
|  | 			continue; | ||||||
|  | 		if (!port->remote) | ||||||
|  | 			continue; | ||||||
|  | 		if (port->remote->sw->is_unplugged) { | ||||||
|  | 			tb_switch_free(port->remote->sw); | ||||||
|  | 			port->remote = NULL; | ||||||
|  | 		} else { | ||||||
|  | 			tb_free_unplugged_children(port->remote->sw); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| /**
 | /**
 | ||||||
|  * find_pci_up_port() - return the first PCIe up port on @sw or NULL |  * find_pci_up_port() - return the first PCIe up port on @sw or NULL | ||||||
|  */ |  */ | ||||||
|  | @ -368,3 +390,42 @@ struct tb *thunderbolt_alloc_and_start(struct tb_nhi *nhi) | ||||||
| 	return NULL; | 	return NULL; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void thunderbolt_suspend(struct tb *tb) | ||||||
|  | { | ||||||
|  | 	tb_info(tb, "suspending...\n"); | ||||||
|  | 	mutex_lock(&tb->lock); | ||||||
|  | 	tb_switch_suspend(tb->root_switch); | ||||||
|  | 	tb_ctl_stop(tb->ctl); | ||||||
|  | 	tb->hotplug_active = false; /* signal tb_handle_hotplug to quit */ | ||||||
|  | 	mutex_unlock(&tb->lock); | ||||||
|  | 	tb_info(tb, "suspend finished\n"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void thunderbolt_resume(struct tb *tb) | ||||||
|  | { | ||||||
|  | 	struct tb_pci_tunnel *tunnel, *n; | ||||||
|  | 	tb_info(tb, "resuming...\n"); | ||||||
|  | 	mutex_lock(&tb->lock); | ||||||
|  | 	tb_ctl_start(tb->ctl); | ||||||
|  | 
 | ||||||
|  | 	/* remove any pci devices the firmware might have setup */ | ||||||
|  | 	tb_switch_reset(tb, 0); | ||||||
|  | 
 | ||||||
|  | 	tb_switch_resume(tb->root_switch); | ||||||
|  | 	tb_free_invalid_tunnels(tb); | ||||||
|  | 	tb_free_unplugged_children(tb->root_switch); | ||||||
|  | 	list_for_each_entry_safe(tunnel, n, &tb->tunnel_list, list) | ||||||
|  | 		tb_pci_restart(tunnel); | ||||||
|  | 	if (!list_empty(&tb->tunnel_list)) { | ||||||
|  | 		/*
 | ||||||
|  | 		 * the pcie links need some time to get going. | ||||||
|  | 		 * 100ms works for me... | ||||||
|  | 		 */ | ||||||
|  | 		tb_info(tb, "tunnels restarted, sleeping for 100ms\n"); | ||||||
|  | 		msleep(100); | ||||||
|  | 	} | ||||||
|  | 	 /* Allow tb_handle_hotplug to progress events */ | ||||||
|  | 	tb->hotplug_active = true; | ||||||
|  | 	mutex_unlock(&tb->lock); | ||||||
|  | 	tb_info(tb, "resume finished\n"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -214,9 +214,14 @@ static inline int tb_port_write(struct tb_port *port, void *buffer, | ||||||
| 
 | 
 | ||||||
| struct tb *thunderbolt_alloc_and_start(struct tb_nhi *nhi); | struct tb *thunderbolt_alloc_and_start(struct tb_nhi *nhi); | ||||||
| void thunderbolt_shutdown_and_free(struct tb *tb); | void thunderbolt_shutdown_and_free(struct tb *tb); | ||||||
|  | void thunderbolt_suspend(struct tb *tb); | ||||||
|  | void thunderbolt_resume(struct tb *tb); | ||||||
| 
 | 
 | ||||||
| struct tb_switch *tb_switch_alloc(struct tb *tb, u64 route); | struct tb_switch *tb_switch_alloc(struct tb *tb, u64 route); | ||||||
| void tb_switch_free(struct tb_switch *sw); | void tb_switch_free(struct tb_switch *sw); | ||||||
|  | void tb_switch_suspend(struct tb_switch *sw); | ||||||
|  | int tb_switch_resume(struct tb_switch *sw); | ||||||
|  | int tb_switch_reset(struct tb *tb, u64 route); | ||||||
| void tb_sw_set_unpplugged(struct tb_switch *sw); | void tb_sw_set_unpplugged(struct tb_switch *sw); | ||||||
| struct tb_switch *get_switch_at_route(struct tb_switch *sw, u64 route); | struct tb_switch *get_switch_at_route(struct tb_switch *sw, u64 route); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Andreas Noever
						Andreas Noever