1.设备树部分
你既然想使能S485,那么必须要去看S485的硬件原理图。我的是这个:
通过图我们看到他是和串口3连在一起的。所以说在设备树上就要将串口3与S485联系起来,我们打开我们板子对应的设备树文件(我的板子100ask_imx6ull_mini.dtb)。这部分有的厂家已经帮你写好了,有的没有写。没有写的你要学会去写设备树文件(自己查视频资料去学)。我的设备树文件如下,翻到UART3部分。大部分已经配置好了。
配置完毕以后咱们下一步。
2.驱动部分
如果只是为了测试,这部分可以不看。因为很多情况是不需要什么修改的。但是深入学习的话还是得理解他。
驱动部分是精华,这部分去看驱动大全之UART子系统-韦东山的视频课。他有很细致的讲解。你需要去了解整个驱动的框架与分层。知道怎么从最下面的设备树开始一步步往上注册相关结构体的。知道上面的函数,比如open/write/如何一步一步向下调用的。学习这些为了是对UART一个比较深入的了解。不然你永远不知道底层怎么运作。
附上UART子系统的框架图(看不懂就去学吧):
UART子系统代码文件如下:
硬件相关: drivers/tty/serial/imx.c 串口核心层: drivers/tty/serial/serial_core.c TTY 层: drivers/tty/tty_io.c
3.应用程序部分
首先我简单介绍一下我的项目:通过S485要发送一串固定的16进制数据出来。外设接受到了这个数据以后能够实现开关机。以及反馈一些设备的信息。
3.1 波特率行规程设置函数
/* set_opt(fd,115200,8,'N',1) */ int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop){ //设置行规程 struct termios newtio,oldtio;//定义两个 newtio,oldtio termios 结构体 // Memset 用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为‘ ’或‘/0’; memset(&oldtio, 0, sizeof(oldtio)); //tcgetattr 函数用于获取与终端相关的参数。参数 fd 为终端的文件描述符,返回的结果保 存在 termios 结构体中,成功返回 0 if ( tcgetattr( fd,&oldtio) != 0) { //获得驱动程序的默认参数放到 oldtio 中,备份下来 //perror ( )用 来 将 上 一 个 函 数 发 生 错 误 的 原 因 输 出 到 标 准 设备 (stderr) 。参数s 所指的字符串会先打印出,后面再加上错误原因字符串。 perror("SetupSerial 1"); return -1; } memset(&newtio, 0, sizeof(newtio)); /* ignore modem control lines and enable receiver */ newtio.c_cflag = newtio.c_cflag |= CLOCAL | CREAD;//CLOCAL :忽略 modem 控制线。CREAD :打开接受者 newtio.c_cflag &= ~CSIZE;//CSIZE:字符长度掩码(传送或接收字元时用的位数)。取值为CS5(传送或接收字元时用 5bits), CS6, CS7, 或 CS8。 /* set character size */ switch( nBits )//设置数据位的个数:要设置为 8 位 { case 7: newtio.c_cflag |= CS7; break; case 8: newtio.c_cflag |= CS8; break; } switch( nEvent )//设置校验位 注意没有校验位 { case 'O': newtio.c_cflag |= PARENB; newtio.c_cflag |= PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break; case 'E': newtio.c_iflag |= (INPCK | ISTRIP); newtio.c_cflag |= PARENB; newtio.c_cflag &= ~PARODD; break; case 'N': newtio.c_cflag &= ~PARENB;//关闭奇偶校验 break; } switch( nSpeed )//设置波特率 { case 2400: cfsetispeed(&newtio, B2400); cfsetospeed(&newtio, B2400); break; case 4800: cfsetispeed(&newtio, B4800); cfsetospeed(&newtio, B4800); break; case 9600: cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break; case 115200: cfsetispeed(&newtio, B115200); cfsetospeed(&newtio, B115200); break; default: cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break; } if( nStop == 1 )//设置停止位 newtio.c_cflag &= ~CSTOPB; else if ( nStop == 2 ) newtio.c_cflag |= CSTOPB; newtio.c_cc[VMIN] = 0; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */ newtio.c_cc[VTIME] = 0; /* 等待第 1 个数据的时间: * 比如 VMIN 设为 10 表示至少读到 10 个数据才返回, * 但是没有数据总不能一直等吧? 可以设置 VTIME(单位是 10 秒) * 假设 VTIME=1,表示: * 10 秒内一个数据都没有的话就返回 * 如果 10 秒内至少读到了 1 个字节,那就继续等待,完全读到 VMIN 个 数据再返回 */ //tcflush 函数用于清空输入 tcflush(fd,TCIFLUSH); if((tcsetattr(fd,TCSANOW,&newtio))!=0)//tcsetattr 函数设置行规程 TCSANOW:不等数据传 输完毕就立即改变属性。 { perror("com set error"); return -1; } //printf("set done!\n"); return 0; }
3.2 打开串口函数
int open_port(char *com) { int fd; //fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);//打开设备结点,O_NOCTTY /* The O_NOCTTY flag tells UNIX that this program doesn't want to be the "controlling terminal" for that port. If you don't specify this then any input (such as keyboard abort signals and so forth) will affect your process. Programs like getty(1M/8) use this feature when starting the login process, but normally a user program does not want this behavior. */ fd = open(com, O_RDWR); if (-1 == fd){ return(-1); } }
3.3 使能S485函数
其实大部分代码是一个串口的程序。区别也就是如下的rs485_enable函数。该函数通过ioctl的方式配置了使能参数。
int rs485_enable(const int fd, const RS485_ENABLE_t enable) { struct serial_rs485 rs485conf; int res; /* Get configure from device */ res = ioctl(fd, TIOCGRS485, &rs485conf); if (res < 0) { perror("Ioctl error on getting 485 configure:"); close(fd); return res; } /* Set enable/disable to configure */ if (enable) { // Enable rs485 mode rs485conf.flags |= SER_RS485_ENABLED; /* 当发送数据时, RTS 为 1 */ rs485conf.flags |= SER_RS485_RTS_ON_SEND; //rs485conf.flags |= SER_RS485_RTS_AFTER_SEND; //rs485conf.flags |= SER_RS485_RX_DURING_TX; } else { // Disable rs485 mode rs485conf.flags &= ~(SER_RS485_ENABLED); } rs485conf.delay_rts_before_send = 0x00000004; /* Set configure to device */ res = ioctl(fd, TIOCSRS485, &rs485conf); if (res < 0) { perror("Ioctl error on setting 485 configure:"); close(fd); } return res; }
查看内核485帮助文档,去进行485函数的一些配置。
如内核提示了SER_RS485_RTS等相关的config设置,这些设置应该就是会影响控制信号的控制。
比如增加了如下标志后,发现发送完成后,控制信号被拉低,可以再次接收数据了。
rs485conf.flags |= SER_RS485_RTS_AFTER_SEND;
内核485帮助文档:
From user-level, RS485 configuration can be get/set using the previous ioctls. For instance, to set RS485 you can use the following code: #include <linux/serial.h> /* Driver-specific ioctls: */ #define TIOCGRS485 0x542E #define TIOCSRS485 0x542F /* Open your specific device (e.g., /dev/mydevice): */ int fd = open ("/dev/mydevice", O_RDWR); if (fd < 0) { /* Error handling. See errno. */ } struct serial_rs485 rs485conf; /* Enable RS485 mode: */ rs485conf.flags |= SER_RS485_ENABLED; /* Set logical level for RTS pin equal to 1 when sending: */ rs485conf.flags |= SER_RS485_RTS_ON_SEND; /* or, set logical level for RTS pin equal to 0 when sending: */ rs485conf.flags &= ~(SER_RS485_RTS_ON_SEND); /* Set logical level for RTS pin equal to 1 after sending: */ rs485conf.flags |= SER_RS485_RTS_AFTER_SEND; /* or, set logical level for RTS pin equal to 0 after sending: */ rs485conf.flags &= ~(SER_RS485_RTS_AFTER_SEND); /* Set rts delay before send, if needed: */ rs485conf.delay_rts_before_send = ...; /* Set rts delay after send, if needed: */ rs485conf.delay_rts_after_send = ...; /* Set this flag if you want to receive data even whilst sending data */ rs485conf.flags |= SER_RS485_RX_DURING_TX; if (ioctl (fd, TIOCSRS485, &rs485conf) < 0) { /* Error handling. See errno. */ } /* Use read() and write() syscalls here... */ /* Close the device when finished: */ if (close (fd) < 0) { /* Error handling. See errno. */ }
3.4 主函数如何写
步骤一 open 设备节点
fd = open_port(argv[1]);//打开逆变器设备节点,以读写的方式打开,可读可写。打开成功 会返回 0 if (fd < 0) {//打开失败 perror("open failed"); return -1; }
步骤二 使能 485
rs485_enable(fd,ENABLE);
步骤三 设置波特率行规程
iRet = set_opt(fd, 2400, 8, 'N', 1);//这就是我要重点关注的,设置比特率位 115200,数据位 为 8,没有校验位,停止位的个数是 1。 if (iRet < 0) { perror("set_port failed"); return -1; }
步骤四 读写你想要的数据
write(fd, &c, 8);//发给指定串口数据 read(fd ,read_buf ,sizeof(read_buf));//读指定串口数据
步骤五 结束恢复旧的配置
/* restore the old configuration */ tcsetattr(fd, TCSANOW, &oldtio); close(fd);
完整代码
我的项目想的完整代码,你可以根据这个去修改成你想要的。
#include <stdio.h> #include <termios.h> #include <linux/ioctl.h> #include <linux/serial.h> #include <asm-generic/ioctls.h> /* TIOCGRS485 + TIOCSRS485 ioctl definitions */ #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <stdlib.h> #include <getopt.h> #include <stdint.h> #include <sys/ioctl.h> /* 数据传输速率:2400 数据编码:除了帧头,帧尾,开关量,命令采用十六进制,其余采用压缩 BCD 表示。 异步,一个起始位,一个停止位,八个数据位,没有校验位 SOI 起始位标志 0XAE ADDR 设备地址描述符 LENGTH CID+INFO 长度 CID 命令 INFO 信息 :0x03 :0x04 :0x83 :0x84 CHKSUM 校验码 EOI 结束码 0XEE */ typedef enum {DISABLE = 0, ENABLE} RS485_ENABLE_t; /* set_opt(fd,115200,8,'N',1) */ int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop) { //设置行规程 struct termios newtio,oldtio;//定义两个 newtio,oldtio termios 结构体 // Memset 用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初 始化为‘ ’或‘/0’; memset(&oldtio, 0, sizeof(oldtio)); //tcgetattr 函数用于获取与终端相关的参数。参数 fd 为终端的文件描述符,返回的结果保 存在 termios 结构体中,成功返回 0 if ( tcgetattr( fd,&oldtio) != 0) { //获得驱动程序的默认参数放到 oldtio 中,备份下来 //perror ( )用 来 将 上 一 个 函 数 发 生 错 误 的 原 因 输 出 到 标 准 设备 (stderr) 。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。 perror("SetupSerial 1"); return -1; } memset(&newtio, 0, sizeof(newtio)); /* ignore modem control lines and enable receiver */ newtio.c_cflag = newtio.c_cflag |= CLOCAL | CREAD;//CLOCAL :忽略 modem 控制线。 CREAD :打开接受者 newtio.c_cflag &= ~CSIZE;//CSIZE:字符长度掩码(传送或接收字元时用的位数)。取值为 CS5(传送或接收字元时用 5bits), CS6, CS7, 或 CS8。 /* set character size */ switch( nBits )//设置数据位的个数:要设置为 8 位 { case 7: newtio.c_cflag |= CS7; break; case 8: newtio.c_cflag |= CS8; break; } switch( nEvent )//设置校验位 注意没有校验位 { case 'O': newtio.c_cflag |= PARENB; newtio.c_cflag |= PARODD; newtio.c_iflag |= (INPCK | ISTRIP); break; case 'E': newtio.c_iflag |= (INPCK | ISTRIP); newtio.c_cflag |= PARENB; newtio.c_cflag &= ~PARODD; break; case 'N': newtio.c_cflag &= ~PARENB;//关闭奇偶校验 break; } switch( nSpeed )//设置波特率 { case 2400: cfsetispeed(&newtio, B2400); cfsetospeed(&newtio, B2400); break; case 4800: cfsetispeed(&newtio, B4800); cfsetospeed(&newtio, B4800); break; case 9600: cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break; case 115200: cfsetispeed(&newtio, B115200); cfsetospeed(&newtio, B115200); break; default: cfsetispeed(&newtio, B9600); cfsetospeed(&newtio, B9600); break; } if( nStop == 1 )//设置停止位 newtio.c_cflag &= ~CSTOPB; else if ( nStop == 2 ) newtio.c_cflag |= CSTOPB; newtio.c_cc[VMIN] = 0; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */ newtio.c_cc[VTIME] = 0; /* 等待第 1 个数据的时间: * 比如 VMIN 设为 10 表示至少读到 10 个数据才返回, * 但是没有数据总不能一直等吧? 可以设置 VTIME(单位是 10 秒) * 假设 VTIME=1,表示: * 10 秒内一个数据都没有的话就返回 * 如果 10 秒内至少读到了 1 个字节,那就继续等待,完全读到 VMIN 个 数据再返回 */ //tcflush 函数用于清空输入 tcflush(fd,TCIFLUSH); if((tcsetattr(fd,TCSANOW,&newtio))!=0)//tcsetattr 函数设置行规程 TCSANOW:不等数据传 输完毕就立即改变属性。 { perror("com set error"); return -1; } //printf("set done!\n"); return 0; } int open_port(char *com) { int fd; //fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY); fd = open(com, O_RDWR);//打开设备结点,O_NOCTTY /* The O_NOCTTY flag tells UNIX that this program doesn't want to be the "controlling terminal" for that port. If you don't specify this then any input (such as keyboard abort signals and so forth) will affect your process. Programs like getty(1M/8) use this feature when starting the login process, but normally a user program does not want this behavior. */ if (-1 == fd){ return(-1); } } //控制逆变器 /** * @brief: print usage message * @Param: stream: output device * @Param: exit_code: error code which want to exit */ /** * @brief: main function * @Param: argc: number of parameters * @Param: argv: parameters list */ int rs485_enable(const int fd, const RS485_ENABLE_t enable) { struct serial_rs485 rs485conf; int res; /* Get configure from device */ res = ioctl(fd, TIOCGRS485, &rs485conf); if (res < 0) { perror("Ioctl error on getting 485 configure:"); close(fd); return res; } /* Set enable/disable to configure */ if (enable) { // Enable rs485 mode rs485conf.flags |= SER_RS485_ENABLED; /* 当发送数据时, RTS 为 1 */ rs485conf.flags |= SER_RS485_RTS_ON_SEND; //rs485conf.flags |= SER_RS485_RTS_AFTER_SEND; //rs485conf.flags |= SER_RS485_RX_DURING_TX; } else { // Disable rs485 mode rs485conf.flags &= ~(SER_RS485_ENABLED); } rs485conf.delay_rts_before_send = 0x00000004; /* Set configure to device */ res = ioctl(fd, TIOCSRS485, &rs485conf); if (res < 0) { perror("Ioctl error on setting 485 configure:"); close(fd); } return res; } /* * ./inverter_test <dev> */ int main(int argc, char *argv[])//两个参数 /* argument count 参数个数 argument value 参数值 */ { char read_buf[1024]; int fd,nread; int iRet; pid_t pid; int next_option; //extern 是一个关键字,它告诉编译器存在着一个变量或者一个函数,如果在当前编译语句的前面中没有找到相应的变量或者函数,也会在当前文件的后面或者其它文件中定义 extern struct termios oldtio; int speed ; char *device; /* 1. open 设备节点 */ fd = open_port(argv[1]);//打开逆变器设备节点,以读写的方式打开,可读可写。打开成功 会返回 0 if (fd < 0) {//打开失败 perror("open failed"); return -1; } /* 3. 使能 485 */ rs485_enable(fd,ENABLE); /* 2. setup 设置 * 115200,8N1 波特率 * RAW mode 行规程 * return data immediately 即时返回 */ iRet = set_opt(fd, 2400, 8, 'N', 1);//这就是我要重点关注的,设置比特率位 115200,数据位 为 8,没有校验位,停止位的个数是 1。 if (iRet < 0) { perror("set_port failed"); return -1; } /*4.读写*/ while (1) { int value; printf("请输入命令\n 开机为 1\n 关机为 0\n"); scanf("%d", &value); uint8_t c[1024]; if(value==1){ c[0]=0xAE; c[1]=0x01; c[2]=0x02; c[3]=0x04; c[4]=0x00; c[5]=0x00; c[6]=0x07; c[7]=0xEE; c[8]='\0'; } else { c[0]=0xAE; c[1]=0x01; c[2]=0x02; c[3]=0x04; c[4]=0x01; c[5]=0x00; c[6]=0x08; c[7]=0xEE; c[8]='\0'; }; //直针 p 初始化为指向 str /* if new data is available on the serial port, read and print it out */ iRet = write(fd, &c, 8);//发给指定串口数据 nread = read(fd ,read_buf ,sizeof(read_buf));//读指定串口数据 if (nread > 0) { printf("RECV[%3d]: ", nread); for(int i = 0; i < nread; i++) printf("0x%02x ", read_buf[i]); printf("\n"); } } /* restore the old configuration */ tcsetattr(fd, TCSANOW, &oldtio); close(fd); return 0 }
4.一些函数的解释
我在这个项目里面遭遇的一些函数罗列一下:
解读PTR_ERR,ERR_PTR,IS_ERR
class类总结
perror()函数的使用
LINUX 使用tcgetattr与tcsetattr函数控制终端
printk函数的用法
read()、write() 相关函数解析
结构体(typedef用法、多维结构体、指针、内嵌函数、赋值)
memset()函数及其作用
内核中修饰的函数的__init的含义
Linux下getopt_long函数的使用
extern及其对struct的使用
有关于fprintf()函数的用法
atoi()函数用法
getopt的用法与optarg
5.通用modbus接口的设计
接下来打算做一个标准的modbus接口。正在学习,慢慢更新。