1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Designware HDMI CEC driver |
4 | * |
5 | * Copyright (C) 2015-2017 Russell King. |
6 | */ |
7 | #include <linux/interrupt.h> |
8 | #include <linux/io.h> |
9 | #include <linux/module.h> |
10 | #include <linux/platform_device.h> |
11 | #include <linux/sched.h> |
12 | #include <linux/slab.h> |
13 | |
14 | #include <drm/drm_edid.h> |
15 | |
16 | #include <media/cec.h> |
17 | #include <media/cec-notifier.h> |
18 | |
19 | #include "dw-hdmi-cec.h" |
20 | |
21 | enum { |
22 | HDMI_IH_CEC_STAT0 = 0x0106, |
23 | HDMI_IH_MUTE_CEC_STAT0 = 0x0186, |
24 | |
25 | HDMI_CEC_CTRL = 0x7d00, |
26 | CEC_CTRL_START = BIT(0), |
27 | CEC_CTRL_FRAME_TYP = 3 << 1, |
28 | CEC_CTRL_RETRY = 0 << 1, |
29 | CEC_CTRL_NORMAL = 1 << 1, |
30 | CEC_CTRL_IMMED = 2 << 1, |
31 | |
32 | HDMI_CEC_STAT = 0x7d01, |
33 | CEC_STAT_DONE = BIT(0), |
34 | CEC_STAT_EOM = BIT(1), |
35 | CEC_STAT_NACK = BIT(2), |
36 | CEC_STAT_ARBLOST = BIT(3), |
37 | CEC_STAT_ERROR_INIT = BIT(4), |
38 | CEC_STAT_ERROR_FOLL = BIT(5), |
39 | CEC_STAT_WAKEUP = BIT(6), |
40 | |
41 | HDMI_CEC_MASK = 0x7d02, |
42 | HDMI_CEC_POLARITY = 0x7d03, |
43 | HDMI_CEC_INT = 0x7d04, |
44 | HDMI_CEC_ADDR_L = 0x7d05, |
45 | HDMI_CEC_ADDR_H = 0x7d06, |
46 | HDMI_CEC_TX_CNT = 0x7d07, |
47 | HDMI_CEC_RX_CNT = 0x7d08, |
48 | HDMI_CEC_TX_DATA0 = 0x7d10, |
49 | HDMI_CEC_RX_DATA0 = 0x7d20, |
50 | HDMI_CEC_LOCK = 0x7d30, |
51 | HDMI_CEC_WKUPCTRL = 0x7d31, |
52 | }; |
53 | |
54 | struct dw_hdmi_cec { |
55 | struct dw_hdmi *hdmi; |
56 | const struct dw_hdmi_cec_ops *ops; |
57 | u32 addresses; |
58 | struct cec_adapter *adap; |
59 | struct cec_msg rx_msg; |
60 | unsigned int tx_status; |
61 | bool tx_done; |
62 | bool rx_done; |
63 | struct cec_notifier *notify; |
64 | int irq; |
65 | |
66 | u8 regs_polarity; |
67 | u8 regs_mask; |
68 | u8 regs_mute_stat0; |
69 | }; |
70 | |
71 | static void dw_hdmi_write(struct dw_hdmi_cec *cec, u8 val, int offset) |
72 | { |
73 | cec->ops->write(cec->hdmi, val, offset); |
74 | } |
75 | |
76 | static u8 dw_hdmi_read(struct dw_hdmi_cec *cec, int offset) |
77 | { |
78 | return cec->ops->read(cec->hdmi, offset); |
79 | } |
80 | |
81 | static int dw_hdmi_cec_log_addr(struct cec_adapter *adap, u8 logical_addr) |
82 | { |
83 | struct dw_hdmi_cec *cec = cec_get_drvdata(adap); |
84 | |
85 | if (logical_addr == CEC_LOG_ADDR_INVALID) |
86 | cec->addresses = 0; |
87 | else |
88 | cec->addresses |= BIT(logical_addr) | BIT(15); |
89 | |
90 | dw_hdmi_write(cec, val: cec->addresses & 255, offset: HDMI_CEC_ADDR_L); |
91 | dw_hdmi_write(cec, val: cec->addresses >> 8, offset: HDMI_CEC_ADDR_H); |
92 | |
93 | return 0; |
94 | } |
95 | |
96 | static int dw_hdmi_cec_transmit(struct cec_adapter *adap, u8 attempts, |
97 | u32 signal_free_time, struct cec_msg *msg) |
98 | { |
99 | struct dw_hdmi_cec *cec = cec_get_drvdata(adap); |
100 | unsigned int i, ctrl; |
101 | |
102 | switch (signal_free_time) { |
103 | case CEC_SIGNAL_FREE_TIME_RETRY: |
104 | ctrl = CEC_CTRL_RETRY; |
105 | break; |
106 | case CEC_SIGNAL_FREE_TIME_NEW_INITIATOR: |
107 | default: |
108 | ctrl = CEC_CTRL_NORMAL; |
109 | break; |
110 | case CEC_SIGNAL_FREE_TIME_NEXT_XFER: |
111 | ctrl = CEC_CTRL_IMMED; |
112 | break; |
113 | } |
114 | |
115 | for (i = 0; i < msg->len; i++) |
116 | dw_hdmi_write(cec, val: msg->msg[i], offset: HDMI_CEC_TX_DATA0 + i); |
117 | |
118 | dw_hdmi_write(cec, val: msg->len, offset: HDMI_CEC_TX_CNT); |
119 | dw_hdmi_write(cec, val: ctrl | CEC_CTRL_START, offset: HDMI_CEC_CTRL); |
120 | |
121 | return 0; |
122 | } |
123 | |
124 | static irqreturn_t dw_hdmi_cec_hardirq(int irq, void *data) |
125 | { |
126 | struct cec_adapter *adap = data; |
127 | struct dw_hdmi_cec *cec = cec_get_drvdata(adap); |
128 | unsigned int stat = dw_hdmi_read(cec, offset: HDMI_IH_CEC_STAT0); |
129 | irqreturn_t ret = IRQ_HANDLED; |
130 | |
131 | if (stat == 0) |
132 | return IRQ_NONE; |
133 | |
134 | dw_hdmi_write(cec, val: stat, offset: HDMI_IH_CEC_STAT0); |
135 | |
136 | if (stat & CEC_STAT_ERROR_INIT) { |
137 | cec->tx_status = CEC_TX_STATUS_ERROR; |
138 | cec->tx_done = true; |
139 | ret = IRQ_WAKE_THREAD; |
140 | } else if (stat & CEC_STAT_DONE) { |
141 | cec->tx_status = CEC_TX_STATUS_OK; |
142 | cec->tx_done = true; |
143 | ret = IRQ_WAKE_THREAD; |
144 | } else if (stat & CEC_STAT_NACK) { |
145 | cec->tx_status = CEC_TX_STATUS_NACK; |
146 | cec->tx_done = true; |
147 | ret = IRQ_WAKE_THREAD; |
148 | } else if (stat & CEC_STAT_ARBLOST) { |
149 | cec->tx_status = CEC_TX_STATUS_ARB_LOST; |
150 | cec->tx_done = true; |
151 | ret = IRQ_WAKE_THREAD; |
152 | } |
153 | |
154 | if (stat & CEC_STAT_EOM) { |
155 | unsigned int len, i; |
156 | |
157 | len = dw_hdmi_read(cec, offset: HDMI_CEC_RX_CNT); |
158 | if (len > sizeof(cec->rx_msg.msg)) |
159 | len = sizeof(cec->rx_msg.msg); |
160 | |
161 | for (i = 0; i < len; i++) |
162 | cec->rx_msg.msg[i] = |
163 | dw_hdmi_read(cec, offset: HDMI_CEC_RX_DATA0 + i); |
164 | |
165 | dw_hdmi_write(cec, val: 0, offset: HDMI_CEC_LOCK); |
166 | |
167 | cec->rx_msg.len = len; |
168 | smp_wmb(); |
169 | cec->rx_done = true; |
170 | |
171 | ret = IRQ_WAKE_THREAD; |
172 | } |
173 | |
174 | return ret; |
175 | } |
176 | |
177 | static irqreturn_t dw_hdmi_cec_thread(int irq, void *data) |
178 | { |
179 | struct cec_adapter *adap = data; |
180 | struct dw_hdmi_cec *cec = cec_get_drvdata(adap); |
181 | |
182 | if (cec->tx_done) { |
183 | cec->tx_done = false; |
184 | cec_transmit_attempt_done(adap, status: cec->tx_status); |
185 | } |
186 | if (cec->rx_done) { |
187 | cec->rx_done = false; |
188 | smp_rmb(); |
189 | cec_received_msg(adap, msg: &cec->rx_msg); |
190 | } |
191 | return IRQ_HANDLED; |
192 | } |
193 | |
194 | static int dw_hdmi_cec_enable(struct cec_adapter *adap, bool enable) |
195 | { |
196 | struct dw_hdmi_cec *cec = cec_get_drvdata(adap); |
197 | |
198 | if (!enable) { |
199 | dw_hdmi_write(cec, val: ~0, offset: HDMI_CEC_MASK); |
200 | dw_hdmi_write(cec, val: ~0, offset: HDMI_IH_MUTE_CEC_STAT0); |
201 | dw_hdmi_write(cec, val: 0, offset: HDMI_CEC_POLARITY); |
202 | |
203 | cec->ops->disable(cec->hdmi); |
204 | } else { |
205 | unsigned int irqs; |
206 | |
207 | dw_hdmi_write(cec, val: 0, offset: HDMI_CEC_CTRL); |
208 | dw_hdmi_write(cec, val: ~0, offset: HDMI_IH_CEC_STAT0); |
209 | dw_hdmi_write(cec, val: 0, offset: HDMI_CEC_LOCK); |
210 | |
211 | dw_hdmi_cec_log_addr(adap: cec->adap, CEC_LOG_ADDR_INVALID); |
212 | |
213 | cec->ops->enable(cec->hdmi); |
214 | |
215 | irqs = CEC_STAT_ERROR_INIT | CEC_STAT_NACK | CEC_STAT_EOM | |
216 | CEC_STAT_ARBLOST | CEC_STAT_DONE; |
217 | dw_hdmi_write(cec, val: irqs, offset: HDMI_CEC_POLARITY); |
218 | dw_hdmi_write(cec, val: ~irqs, offset: HDMI_CEC_MASK); |
219 | dw_hdmi_write(cec, val: ~irqs, offset: HDMI_IH_MUTE_CEC_STAT0); |
220 | } |
221 | return 0; |
222 | } |
223 | |
224 | static const struct cec_adap_ops dw_hdmi_cec_ops = { |
225 | .adap_enable = dw_hdmi_cec_enable, |
226 | .adap_log_addr = dw_hdmi_cec_log_addr, |
227 | .adap_transmit = dw_hdmi_cec_transmit, |
228 | }; |
229 | |
230 | static void dw_hdmi_cec_del(void *data) |
231 | { |
232 | struct dw_hdmi_cec *cec = data; |
233 | |
234 | cec_delete_adapter(adap: cec->adap); |
235 | } |
236 | |
237 | static int dw_hdmi_cec_probe(struct platform_device *pdev) |
238 | { |
239 | struct dw_hdmi_cec_data *data = dev_get_platdata(dev: &pdev->dev); |
240 | struct dw_hdmi_cec *cec; |
241 | int ret; |
242 | |
243 | if (!data) |
244 | return -ENXIO; |
245 | |
246 | /* |
247 | * Our device is just a convenience - we want to link to the real |
248 | * hardware device here, so that userspace can see the association |
249 | * between the HDMI hardware and its associated CEC chardev. |
250 | */ |
251 | cec = devm_kzalloc(dev: &pdev->dev, size: sizeof(*cec), GFP_KERNEL); |
252 | if (!cec) |
253 | return -ENOMEM; |
254 | |
255 | cec->irq = data->irq; |
256 | cec->ops = data->ops; |
257 | cec->hdmi = data->hdmi; |
258 | |
259 | platform_set_drvdata(pdev, data: cec); |
260 | |
261 | dw_hdmi_write(cec, val: 0, offset: HDMI_CEC_TX_CNT); |
262 | dw_hdmi_write(cec, val: ~0, offset: HDMI_CEC_MASK); |
263 | dw_hdmi_write(cec, val: ~0, offset: HDMI_IH_MUTE_CEC_STAT0); |
264 | dw_hdmi_write(cec, val: 0, offset: HDMI_CEC_POLARITY); |
265 | |
266 | cec->adap = cec_allocate_adapter(ops: &dw_hdmi_cec_ops, priv: cec, name: "dw_hdmi" , |
267 | CEC_CAP_DEFAULTS | |
268 | CEC_CAP_CONNECTOR_INFO, |
269 | CEC_MAX_LOG_ADDRS); |
270 | if (IS_ERR(ptr: cec->adap)) |
271 | return PTR_ERR(ptr: cec->adap); |
272 | |
273 | /* override the module pointer */ |
274 | cec->adap->owner = THIS_MODULE; |
275 | |
276 | ret = devm_add_action_or_reset(&pdev->dev, dw_hdmi_cec_del, cec); |
277 | if (ret) |
278 | return ret; |
279 | |
280 | ret = devm_request_threaded_irq(dev: &pdev->dev, irq: cec->irq, |
281 | handler: dw_hdmi_cec_hardirq, |
282 | thread_fn: dw_hdmi_cec_thread, IRQF_SHARED, |
283 | devname: "dw-hdmi-cec" , dev_id: cec->adap); |
284 | if (ret < 0) |
285 | return ret; |
286 | |
287 | cec->notify = cec_notifier_cec_adap_register(hdmi_dev: pdev->dev.parent, |
288 | NULL, adap: cec->adap); |
289 | if (!cec->notify) |
290 | return -ENOMEM; |
291 | |
292 | ret = cec_register_adapter(adap: cec->adap, parent: pdev->dev.parent); |
293 | if (ret < 0) { |
294 | cec_notifier_cec_adap_unregister(n: cec->notify, adap: cec->adap); |
295 | return ret; |
296 | } |
297 | |
298 | /* |
299 | * CEC documentation says we must not call cec_delete_adapter |
300 | * after a successful call to cec_register_adapter(). |
301 | */ |
302 | devm_remove_action(dev: &pdev->dev, action: dw_hdmi_cec_del, data: cec); |
303 | |
304 | return 0; |
305 | } |
306 | |
307 | static void dw_hdmi_cec_remove(struct platform_device *pdev) |
308 | { |
309 | struct dw_hdmi_cec *cec = platform_get_drvdata(pdev); |
310 | |
311 | cec_notifier_cec_adap_unregister(n: cec->notify, adap: cec->adap); |
312 | cec_unregister_adapter(adap: cec->adap); |
313 | } |
314 | |
315 | static int __maybe_unused dw_hdmi_cec_resume(struct device *dev) |
316 | { |
317 | struct dw_hdmi_cec *cec = dev_get_drvdata(dev); |
318 | |
319 | /* Restore logical address */ |
320 | dw_hdmi_write(cec, val: cec->addresses & 255, offset: HDMI_CEC_ADDR_L); |
321 | dw_hdmi_write(cec, val: cec->addresses >> 8, offset: HDMI_CEC_ADDR_H); |
322 | |
323 | /* Restore interrupt status/mask registers */ |
324 | dw_hdmi_write(cec, val: cec->regs_polarity, offset: HDMI_CEC_POLARITY); |
325 | dw_hdmi_write(cec, val: cec->regs_mask, offset: HDMI_CEC_MASK); |
326 | dw_hdmi_write(cec, val: cec->regs_mute_stat0, offset: HDMI_IH_MUTE_CEC_STAT0); |
327 | |
328 | return 0; |
329 | } |
330 | |
331 | static int __maybe_unused dw_hdmi_cec_suspend(struct device *dev) |
332 | { |
333 | struct dw_hdmi_cec *cec = dev_get_drvdata(dev); |
334 | |
335 | /* store interrupt status/mask registers */ |
336 | cec->regs_polarity = dw_hdmi_read(cec, offset: HDMI_CEC_POLARITY); |
337 | cec->regs_mask = dw_hdmi_read(cec, offset: HDMI_CEC_MASK); |
338 | cec->regs_mute_stat0 = dw_hdmi_read(cec, offset: HDMI_IH_MUTE_CEC_STAT0); |
339 | |
340 | return 0; |
341 | } |
342 | |
343 | static const struct dev_pm_ops dw_hdmi_cec_pm = { |
344 | SET_SYSTEM_SLEEP_PM_OPS(dw_hdmi_cec_suspend, dw_hdmi_cec_resume) |
345 | }; |
346 | |
347 | static struct platform_driver dw_hdmi_cec_driver = { |
348 | .probe = dw_hdmi_cec_probe, |
349 | .remove_new = dw_hdmi_cec_remove, |
350 | .driver = { |
351 | .name = "dw-hdmi-cec" , |
352 | .pm = &dw_hdmi_cec_pm, |
353 | }, |
354 | }; |
355 | module_platform_driver(dw_hdmi_cec_driver); |
356 | |
357 | MODULE_AUTHOR("Russell King <rmk+kernel@armlinux.org.uk>" ); |
358 | MODULE_DESCRIPTION("Synopsys Designware HDMI CEC driver for i.MX" ); |
359 | MODULE_LICENSE("GPL" ); |
360 | MODULE_ALIAS(PLATFORM_MODULE_PREFIX "dw-hdmi-cec" ); |
361 | |