summaryrefslogtreecommitdiff
path: root/drivers/tty/serial/atmel_serial.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/tty/serial/atmel_serial.c')
-rw-r--r--drivers/tty/serial/atmel_serial.c87
1 files changed, 81 insertions, 6 deletions
diff --git a/drivers/tty/serial/atmel_serial.c b/drivers/tty/serial/atmel_serial.c
index 7450d3853031..bd07f79a2df9 100644
--- a/drivers/tty/serial/atmel_serial.c
+++ b/drivers/tty/serial/atmel_serial.c
@@ -15,6 +15,7 @@
#include <linux/init.h>
#include <linux/serial.h>
#include <linux/clk.h>
+#include <linux/clk-provider.h>
#include <linux/console.h>
#include <linux/sysrq.h>
#include <linux/tty_flip.h>
@@ -110,6 +111,7 @@ struct atmel_uart_char {
struct atmel_uart_port {
struct uart_port uart; /* uart */
struct clk *clk; /* uart clock */
+ struct clk *gclk; /* uart generic clock */
int may_wakeup; /* cached value of device_may_wakeup for times we need to disable it */
u32 backup_imr; /* IMR saved during suspend */
int break_active; /* break being received */
@@ -150,6 +152,7 @@ struct atmel_uart_port {
u32 rts_low;
bool ms_irq_enabled;
u32 rtor; /* address of receiver timeout register if it exists */
+ bool is_usart;
bool has_frac_baudrate;
bool has_hw_timer;
struct timer_list uart_timer;
@@ -228,6 +231,11 @@ static inline int atmel_uart_is_half_duplex(struct uart_port *port)
(port->iso7816.flags & SER_ISO7816_ENABLED);
}
+static inline int atmel_error_rate(int desired_value, int actual_value)
+{
+ return 100 - (desired_value * 100) / actual_value;
+}
+
#ifdef CONFIG_SERIAL_ATMEL_PDC
static bool atmel_use_pdc_rx(struct uart_port *port)
{
@@ -1825,6 +1833,7 @@ static void atmel_get_ip_name(struct uart_port *port)
*/
atmel_port->has_frac_baudrate = false;
atmel_port->has_hw_timer = false;
+ atmel_port->is_usart = false;
if (name == new_uart) {
dev_dbg(port->dev, "Uart with hw timer");
@@ -1834,6 +1843,7 @@ static void atmel_get_ip_name(struct uart_port *port)
dev_dbg(port->dev, "Usart\n");
atmel_port->has_frac_baudrate = true;
atmel_port->has_hw_timer = true;
+ atmel_port->is_usart = true;
atmel_port->rtor = ATMEL_US_RTOR;
version = atmel_uart_readl(port, ATMEL_US_VERSION);
switch (version) {
@@ -1863,6 +1873,7 @@ static void atmel_get_ip_name(struct uart_port *port)
dev_dbg(port->dev, "This version is usart\n");
atmel_port->has_frac_baudrate = true;
atmel_port->has_hw_timer = true;
+ atmel_port->is_usart = true;
atmel_port->rtor = ATMEL_US_RTOR;
break;
case 0x203:
@@ -2113,6 +2124,8 @@ static void atmel_serial_pm(struct uart_port *port, unsigned int state,
* This is called on uart_close() or a suspend event.
*/
clk_disable_unprepare(atmel_port->clk);
+ if (__clk_is_enabled(atmel_port->gclk))
+ clk_disable_unprepare(atmel_port->gclk);
break;
default:
dev_err(port->dev, "atmel_serial: unknown pm %d\n", state);
@@ -2122,19 +2135,25 @@ static void atmel_serial_pm(struct uart_port *port, unsigned int state,
/*
* Change the port parameters
*/
-static void atmel_set_termios(struct uart_port *port, struct ktermios *termios,
- struct ktermios *old)
+static void atmel_set_termios(struct uart_port *port,
+ struct ktermios *termios,
+ const struct ktermios *old)
{
struct atmel_uart_port *atmel_port = to_atmel_uart_port(port);
unsigned long flags;
- unsigned int old_mode, mode, imr, quot, baud, div, cd, fp = 0;
+ unsigned int old_mode, mode, imr, quot, div, cd, fp = 0;
+ unsigned int baud, actual_baud, gclk_rate;
+ int ret;
/* save the current mode register */
mode = old_mode = atmel_uart_readl(port, ATMEL_US_MR);
/* reset the mode, clock divisor, parity, stop bits and data size */
- mode &= ~(ATMEL_US_USCLKS | ATMEL_US_CHRL | ATMEL_US_NBSTOP |
- ATMEL_US_PAR | ATMEL_US_USMODE);
+ if (atmel_port->is_usart)
+ mode &= ~(ATMEL_US_NBSTOP | ATMEL_US_PAR | ATMEL_US_CHRL |
+ ATMEL_US_USCLKS | ATMEL_US_USMODE);
+ else
+ mode &= ~(ATMEL_UA_BRSRCCK | ATMEL_US_PAR | ATMEL_UA_FILTER);
baud = uart_get_baud_rate(port, termios, old, 0, port->uartclk / 16);
@@ -2282,10 +2301,60 @@ static void atmel_set_termios(struct uart_port *port, struct ktermios *termios,
cd = uart_get_divisor(port, baud);
}
- if (cd > 65535) { /* BRGR is 16-bit, so switch to slower clock */
+ /*
+ * If the current value of the Clock Divisor surpasses the 16 bit
+ * ATMEL_US_CD mask and the IP is USART, switch to the Peripheral
+ * Clock implicitly divided by 8.
+ * If the IP is UART however, keep the highest possible value for
+ * the CD and avoid needless division of CD, since UART IP's do not
+ * support implicit division of the Peripheral Clock.
+ */
+ if (atmel_port->is_usart && cd > ATMEL_US_CD) {
cd /= 8;
mode |= ATMEL_US_USCLKS_MCK_DIV8;
+ } else {
+ cd = min_t(unsigned int, cd, ATMEL_US_CD);
+ }
+
+ /*
+ * If there is no Fractional Part, there is a high chance that
+ * we may be able to generate a baudrate closer to the desired one
+ * if we use the GCLK as the clock source driving the baudrate
+ * generator.
+ */
+ if (!atmel_port->has_frac_baudrate) {
+ if (__clk_is_enabled(atmel_port->gclk))
+ clk_disable_unprepare(atmel_port->gclk);
+ gclk_rate = clk_round_rate(atmel_port->gclk, 16 * baud);
+ actual_baud = clk_get_rate(atmel_port->clk) / (16 * cd);
+ if (gclk_rate && abs(atmel_error_rate(baud, actual_baud)) >
+ abs(atmel_error_rate(baud, gclk_rate / 16))) {
+ clk_set_rate(atmel_port->gclk, 16 * baud);
+ ret = clk_prepare_enable(atmel_port->gclk);
+ if (ret)
+ goto gclk_fail;
+
+ if (atmel_port->is_usart) {
+ mode &= ~ATMEL_US_USCLKS;
+ mode |= ATMEL_US_USCLKS_GCLK;
+ } else {
+ mode |= ATMEL_UA_BRSRCCK;
+ }
+
+ /*
+ * Set the Clock Divisor for GCLK to 1.
+ * Since we were able to generate the smallest
+ * multiple of the desired baudrate times 16,
+ * then we surely can generate a bigger multiple
+ * with the exact error rate for an equally increased
+ * CD. Thus no need to take into account
+ * a higher value for CD.
+ */
+ cd = 1;
+ }
}
+
+gclk_fail:
quot = cd | fp << ATMEL_US_FP_OFFSET;
if (!(port->iso7816.flags & SER_ISO7816_ENABLED))
@@ -2881,6 +2950,12 @@ static int atmel_serial_probe(struct platform_device *pdev)
if (ret)
goto err;
+ atmel_port->gclk = devm_clk_get_optional(&pdev->dev, "gclk");
+ if (IS_ERR(atmel_port->gclk)) {
+ ret = PTR_ERR(atmel_port->gclk);
+ goto err_clk_disable_unprepare;
+ }
+
ret = atmel_init_port(atmel_port, pdev);
if (ret)
goto err_clk_disable_unprepare;