项目总结:i.mx6ull基于S485控制外设

本文涉及的产品
视频直播,500GB 1个月
简介: 项目总结:i.mx6ull基于S485控制外设

1.设备树部分


你既然想使能S485,那么必须要去看S485的硬件原理图。我的是这个:

1670945077824.jpg

通过图我们看到他是和串口3连在一起的。所以说在设备树上就要将串口3与S485联系起来,我们打开我们板子对应的设备树文件(我的板子100ask_imx6ull_mini.dtb)。这部分有的厂家已经帮你写好了,有的没有写。没有写的你要学会去写设备树文件(自己查视频资料去学)。我的设备树文件如下,翻到UART3部分。大部分已经配置好了。

1670945093166.jpg

1670945100282.jpg

配置完毕以后咱们下一步。


2.驱动部分


如果只是为了测试,这部分可以不看。因为很多情况是不需要什么修改的。但是深入学习的话还是得理解他。


驱动部分是精华,这部分去看驱动大全之UART子系统-韦东山的视频课。他有很细致的讲解。你需要去了解整个驱动的框架与分层。知道怎么从最下面的设备树开始一步步往上注册相关结构体的。知道上面的函数,比如open/write/如何一步一步向下调用的。学习这些为了是对UART一个比较深入的了解。不然你永远不知道底层怎么运作。


附上UART子系统的框架图(看不懂就去学吧):

1670945129021.jpg

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接口。正在学习,慢慢更新。

相关文章
|
6月前
Rockchip系列之LED状态灯 串口收发数据流程以及控制状态显示(3)
Rockchip系列之LED状态灯 串口收发数据流程以及控制状态显示(3)
140 0
|
6月前
|
传感器 芯片
STM32--PWR电源控制
STM32--PWR电源控制
129 0
STM32--PWR电源控制
|
Linux 芯片
鸿蒙移植i.mx6ull(十) 系统时钟
鸿蒙移植i.mx6ull(十) 系统时钟
303 0
鸿蒙移植i.mx6ull(十) 系统时钟
|
算法 芯片 异构计算
通过FPGA实现基于RS232串口的指令发送并控制显示器中目标位置
通过FPGA实现基于RS232串口的指令发送并控制显示器中目标位置
|
2月前
|
Linux
使用funcgraph-retval和bpftrace/kprobe快速定位并解决cpu控制器无法使能的问题
使用funcgraph-retval和bpftrace/kprobe快速定位并解决cpu控制器无法使能的问题
|
6月前
Rockchip系列之LED状态灯 CAN收发数据流程以及控制状态显示(4)
Rockchip系列之LED状态灯 CAN收发数据流程以及控制状态显示(4)
150 3
|
6月前
|
传感器 监控 Windows
GE通用电气 IC660BBD022 16电路启用源 I/O模块
GE通用电气 IC660BBD022 16电路启用源 I/O模块
51单片机--利用独立按键控制LED
51单片机--利用独立按键控制LED
293 0
|
定位技术 芯片
ARM架构与编程(基于I.MX6ULL): 串口UART编程(七)(上)
ARM架构与编程(基于I.MX6ULL): 串口UART编程(七)
277 1
ARM架构与编程(基于I.MX6ULL): 串口UART编程(七)(上)