抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

串口是很常用的一个外设,在 Linux 下通常通过串口和其他设备或传感器进行通信,根据电平的不同,串口分为 TTL 和 RS232。不管是什么样的接口电平,其驱动程序都是一样的,通过外接 RS485 这样 的芯片就可以将串口转换为 RS485 信号,正点原子的 I.MX6U-ALPHA 开发板就是这么做的。对于正点原子的 I.MX6U-ALPHA 开发板而言, RS232、 RS485 以及 GPS 模块接口通通连接到了 I.MX6U 的 UART3 接口上,因此这些外设最终都归结为 UART3 的串口驱动。

1Linux下UART驱动框架

1.1 UART驱动框架简介

同 I2C、 SPI 一样, Linux 也提供了串口驱动框架,我们只需要按照相应的串口框架编写驱动程序即可。串口驱动没有什么主机端和设备端之分,就只有一个串口驱动,而且这个驱动也已经由 NXP 官方已经编写好了,我们真正要做的就是在设备树中添加所要使用的串口节点信息。当系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成/dev/ttymxcX(X=0….n)文件。

uart_driver 结构体表示 UART 驱动, uart_driver 定义在 include/linux/serial_core.h 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct uart_driver {
struct module *owner; /* 模块所属者 */
const char *driver_name; /* 驱动名字 */
const char *dev_name; /* 设备名字 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
int nr; /* 设备数 */
struct console *cons; /* 控制台 */

/*
* these are private; the low level driver should not
* touch these; they should be initialised to NULL
*/
struct uart_state *state;
struct tty_driver *tty_driver;
};

1.2 uart_driver 注册与注销

每个串口驱动都需要定义一个 uart_driver,加载驱动的时候通过 uart_register_driver 函数向系统注册这个 uart_driver,此函数原型如下:

1
int uart_register_driver(struct uart_driver *drv)
  • drv: 要注册的 uart_driver

  • 返回值: 0,成功;负值,失败。

注销驱动的时候也需要注销掉前面注册的 uart_driver,需要用到 uart_unregister_driver 函数,函数原型如下:

1
void uart_unregister_driver(struct uart_driver *drv)
  • drv: 要注销的 uart_driver
  • 返回值: 无。

1.3 uart_port 的添加与移除

uart_port 表示一个具体的 串口端口, uart_port 定义在 include/linux/serial_core.h 文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct uart_port {
spinlock_t lock; /* port lock */
unsigned long iobase; /* in/out[bwl] */
unsigned char __iomem *membase; /* read/write[bwl] */
............
const struct uart_ops *ops;
unsigned int custom_divisor;
unsigned int line; /* port index */
unsigned int minor;
resource_size_t mapbase; /* for ioremap */
resource_size_t mapsize;
struct device *dev; /* parent device */
............
};
  • ops 包含了串口的具体驱动函数。

**每个 UART 都有一个 uart_port**,那么 uart_port 是怎么和 uart_driver 结合起来的呢?这里要用到 uart_add_one_port 函数,函数原型如下:

1
2
int uart_add_one_port(struct uart_driver *drv,
struct uart_port *uport)
  • drv:此 port 对应的 uart_driver
  • uport: 要添加到 uart_driver 中的 port。
  • 返回值: 0,成功;负值,失败。

卸载 UART 驱动的时候也需要将 uart_port 从相应的 uart_driver 中移除,需要用到uart_remove_one_port 函数,函数原型如下:

1
int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport)
  • drv:要卸载的 port 所对应的 uart_driver
  • uport: 要卸载的 uart_port
  • 返回值:0,成功;负值,失败。

1.4 uart_ops 实现

ops 包含了针对 UART 具体的驱动函数, Linux 系统收发数据最终调用的都是 ops 中的函数。 ops 是 uart_ops类型的结构体指针变量, uart_ops 定义在 include/linux/serial_core.h 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*throttle)(struct uart_port *);
void (*unthrottle)(struct uart_port *);
void (*send_xchar)(struct uart_port *, char ch);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int ctl);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*flush_buffer)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *new,
struct ktermios *old);
void (*set_ldisc)(struct uart_port *, struct ktermios *);
void (*pm)(struct uart_port *, unsigned int state,
unsigned int oldstate);

/*
* Return a string describing the type of the port
*/
const char *(*type)(struct uart_port *);

/*
* Release IO and memory resources used by the port.
* This includes iounmap if necessary.
*/
void (*release_port)(struct uart_port *);

/*
* Request IO and memory resources used by the port.
* This includes iomapping the port if necessary.
*/
int (*request_port)(struct uart_port *);
void (*config_port)(struct uart_port *, int);
int (*verify_port)(struct uart_port *, struct serial_struct *);
int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct uart_port *);
void (*poll_put_char)(struct uart_port *, unsigned char);
int (*poll_get_char)(struct uart_port *);
#endif
};

UART 驱动编写人员需要实现 uart_ops,因为 uart_ops 是最底层的 UART 驱动接口,是实实在在的和 UART 寄存器打交道的。

关于 uart_ops 结构体中的这些函数的具体含义请参考Documentation/serial/driver 这个文档

2 NXP官方驱动分析

打开 imx6ull.dtsi 文件,找到 UART3 对应的子节点,子节点内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
uart3: serial@021ec000 {
compatible = "fsl,imx6ul-uart",
"fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x021ec000 0x4000>;
interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART3_IPG>,
<&clks IMX6UL_CLK_UART3_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 29 4 0>, <&sdma 30 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};

查找compatible属性,找到imx.c里面:

1
2
3
4
5
6
static const struct of_device_id imx_uart_dt_ids[] = {
{ .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
{ .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], },
{ .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
{ /* sentinel */ }
};

NXP官方串口驱动入口函数为:imx_serial_init,此函数调用uart_register_driver先内核注册uart_driver,为imx_reg,因为#define DEV_NAME "ttymxc",所以IMX串口为/dev/ttymxc0,1。

接下来就是uart_port处理,NXP自定义了一个imx_port,里面包含uart_port

uart_opssport->port.ops = &imx_pops;

串口接收中断处理函数:imx_rxint获取到串口接收到的数据,然后使用tty_insert_flip_char将其放到tty里面。

3 设备树修改

添加节点:

1
2
3
4
5
&uart3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart3>;
status = "okay";
};
1
2
3
4
5
6
pinctrl_uart3: uart3grp {
fsl,pins = <
MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX 0x1b0b1
MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX 0x1b0b1
>;
};

4 移植minicom

minicom 类似我们常用的串口调试助手,是 Linux 下很常用的一个串口工具,将 minicom移植到我们的开发板中,这样我们就可以借助 minicom 对串口进行读写操作。

4.1移植 ncurses

minicom 需要用到 ncurses, 因此需要先移植 ncurses,如果前面已经移植好了 ncurses,那么这里就不需要再次移植了,只需要在编译 minicom 的时候指定 ncurses 库和头文件目录 即可。

  • 首先在ubuntu中创建一个目录来存放我们要移植的文件。

  • 下载 ncurses 源码。

  • ncurses-6.0.tar.gz 拷贝到 Ubuntu 中创建的 tool 目录下,然后进行解压,解压命令如下:

    1
    tar -vxzf ncurses-6.0.tar.gz
  • 解压完成以后就会生成一个名为“ncurses-6.0”的文件夹,此文件夹就是 ncurese 的源码文件夹。在 tool 目录下新建名为“ncurses”目录,用于保存 ncurses 编译结果,一切准备就绪以后就可以编译 ncureses 库了。进入到 ncureses 源码目录下,也就是刚刚解压出来的 ncurses-6.0 目录中,首先是配置 ncureses,输入如下命令:

    1
    ./configure --prefix=/home/zwl/linux/tool/ncurses --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf --with-shared --without-profile --disable-stripping --without-progs --with-manpages --without-tests
    • configure 就是配置脚本。
    • –prefix 用于指定编译结果的保存目录,这里肯定将编译结果保存到我们前面创建的“ ncurses”目录中。
    • –host 用于指定编译器前缀,这里设置为 “arm-linuxgnueabihf”。
    • –target 用于指定目标,这里也设置为“arm-linux-gnueabihf”。
  • 配置成功以后输入“make”命令开始编译 。

  • 编译成功以后输入“make install”命令安装,安装的意思就是将编译出来的结果拷贝到–pfefix 指定的目录里面去。

  • 安装成功以后查看一下前面创建的“ncurses”文件夹,会发现里面多了一些东西。

    • image-20250105131054350
  • include、 lib 和 share 这三个目录中存放的文件分别拷贝到开发板根文件系统中的/usr/include、 /usr/lib 和/usr/share 这三个目录中,如果哪个目录不存在的话请自行创建!!拷贝命令如下:

    1
    2
    3
    sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/usr/lib/ -rfa
    sudo cp share/* /home/zuozhongkai/linux/nfs/rootfs/usr/share/ -rfa
    sudo cp include/* /home/zuozhongkai/linux/nfs/rootfs/usr/include/ -rfa
  • 然后在开发板根目录的/etc/profile(没有的话自己创建一个)文件中添加如下所示内容:

    1
    2
    3
    4
    5
    6
    #!/bin/sh
    LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
    export LD_LIBRARY_PATH

    export TERM=vt100
    export TERMINFO=/usr/share/terminfo

4.2 移植minicom

  • 将 minicom-2.7.1.tar.gz 拷贝到 ubuntu 中的/home/zuozhongkai/linux/IMX6ULL/tool 目录下,然后在 tool 目录下新建一个名为“minicom”的子目录,用于存放 minicom 编译结果。一切准备好以后就可以编译 minicom 了,先解压 minicom,命令如下:

    1
    tar -vxzf minicom-2.7.1.tar.gz
  • 解压完成以后会生成一个叫做 minicom-2.7.1 的文件夹,这个就是 minicom 的源码,进入到此目录中,然后配置 minicom,配置命令如下:

    1
    ./configure CC=arm-linux-gnueabihf-gcc -- prefix=/home/zwl/linux/IMX6ULL/tool/minicom --host=arm-linux-gnueabihf CPPFLAGS=-I/home/zwl/linux/IMX6ULL/tool/ncurses/include LDFLAGS=-L/home/zuozhongkai/linux/IMX6ULL/tool/ncurses/lib -enable-cfg-dir=/etc/minicom 	
    • CC 表示要使用的 gcc 交叉编译器。
    • –prefix 指定编译出来的文件存放目录,肯定要存放到我们前面创建的 minicom 目录中。
    • –host 指定交叉编译器前缀。
    • CPPFLAGS 指定 ncurses 的头文件路径。
    • LDFLAGS 指定 ncurses 的库路径。
  • 配置成功以后执行如下命令编译并安装:

    1
    2
    make
    make install
  • 将 minicom 目录中 bin 子目录下的所有文件拷贝到开发板根目录中的/usr/bin 目录下,命令如下:

    1
    sudo cp bin/* /home/zwl/linux/nfs/rootfs/usr/bin/
  • 完成以后在开发板中输入“minicom -v”来查看 minicom 工作是否正常 。

    image-20250105150904956

  • 从图可以看出,此时 minicom 版本号为 2.7.1, minicom 版本号查看正常。输入如下命令打开 minicom 配置界面:

    1
    minicom -s

    image-20250105150941931

  • 打开失败,新建/etc/passwd 文件,然后在 passwd 文件里面输入如下所示内容:

    1
    root:x:0:0:root:/root:/bin/sh
  • 开发板重启以后再执行“minicom -s”命令,此时 minicom 配置界面就可以打开了。

    image-20250105151200159
  • 如果能出现图上所示界面,那么就说明 mincom 工作正常了。

5 驱动测试

5.1minicom配置

  • 打开 minicom 配置界面,然后选中“Serial port setup” 。

  • 选中“Serial port setup”以后点击回车,进入设置菜单

    image-20250105152254208

    • 图中有 7 个设置项目,分别对应 A、 B……G,比如第一个是选中串口, UART3 的串口文件为/dev/ttymxc2,因此串口设备要设置/dev/ttymxc2。设置方法就是按下键盘上的‘A’,然后输入“/dev/ttymxc2”即可,都设置完成以后按下回车键确认并退出。
    • 设置完以后按下回车键确认,确认完以后就可以设置其他的配置项。比如 E 设置波特率、数据位和停止位的、 F 设置硬件流控的,设置方法都一样。
  • 按下ESC 键退出,退出后显示下图所示串口调试界面,可以看出当前的串口文件为/dev/ttymxc2,按下 CTRLA,然后再按下 Z 就可以打开 minicom 帮助信息界面。 image-20250105153025699

  • 可以看出, minicom 有很多快捷键,本实验我们打开 minicom 的回显功能,回显功能配置项为“local Echo on/off..E”,因此按下 E 即可打开/关闭回显功能。

    image-20250105153252222

5.2RS232测试

mini板没有RS232。放弃。

5.3RS485测试

自己没有测试模块。遂放弃。

5.4GPS测试

GPS买了没啥用,还挺贵,就为了做个实验不值当。还是放弃。

5.5串口测试

USB转TTL还是有的。可以做测试。

开发板连接:TX->RX,RX->TX。

直接输入,会看到串口工具会显示。并且收发都能成功。

image-20250105154108576 image-20250105154303007

评论