第4章 Linux串口应用编程
参考资料(您可识别下方二维码或复制链接至浏览器中打开查看)
https://digilander.libero.it/robang/rubrica/serial.htm
https://www.cnblogs.com/feisky/archive/2010/05/21/1740893.html
https://blog.csdn.net/yemingzhu163/article/details/5897156
4.1 串口API
图4.1 Linux串口通信
在Linux系统中,操作设备的统一接口就是:open/ioctl/read/write。对于UART,又在ioctl之上封装了很多函数,主要是用来设置行规程。所以对于UART,编程的套路就是:
⚫ open;
⚫ 设置行规程,比如波特率、数据位、停止位、检验位、RAW模式、一有数据就返回;
⚫ read/write
怎么设置行规程?行规程的参数用结构体termios来表示,可以参考Linux串口—struct termios结构体。
您可识别下方二维码或复制链接至浏览器中打开查看:
https://blog.csdn.net/yemingzhu163/article/details/5897156
这些函数在名称上有一些惯例:
▪ tc:terminal contorl
▪ cf:control flag
下面列出一些函数:
表4-1 行规程函数
函数不多,主要是需要设置好termios中的参数,这些参数很复杂,可以参考Linux串口—struct termios结构体。
4.2 普通串应用编程(收发实验)
本实验通过把串口的发送、接收引脚短接,实现自发自收:使用write函数发出字符,使用read函数读取字符。
源码位于如下目录:
4.2.1 硬件连接
打开瑞米派原理图“myb-yg2lx-remi-v10-1226.pdf”,把下图的UART_TX、UART_RX短接:
板子上的操作如下图所示:
4.2.2 程序分析
1. 程序主流程分析
当我们执行“./serial_send_recv/dev/ttySC4”命令时,main 函数会首先打开串口、设置串口参数,代码如下:
/*
* ./serial_send_recv
*/
int main(int argc, char **argv)
{
int fd;
int iRet;
char c;
/* 1. open */
/* 2. setup
* 115200,8N1
* RAW mode
* return data immediately
*/
/* 3. write and read */
if (argc != 2)
{
printf("Usage: \n");
printf("%s \n", argv[0]);
return -1;
}
fd = open_port(argv[1]);
if (fd < 0)
{
printf("open %s err!\n", argv[1]);
return -1;
}
iRet = set_opt(fd, 115200, 8, 'N', 1);
if (iRet)
{
printf("set port err!\n");
return -1;
}
第27行:打开串口(open_port函数后面再分析)。
第34行:设置串口参数(set_opt函数后面再分析)。
然后就可以往串口里发送数据、从串口读取数据了,代码如下:
printf("Enter a char: ");
while (1)
{
scanf("%c", &c);
iRet = write(fd, &c, 1);
iRet = read(fd, &c, 1);
if (iRet == 1)
printf("get: %02x %c\n", c, c);
else
printf("can not get data\n");
}
第4行:从控制台读取1个字符。
第5行:把这个字符写入串口。
第6行:读取串口,在硬件回环的情况下,会读到这个字符。
2. open_port函数分析
代码如下:
int open_port(char *com)
{
int fd;
//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
fd = open(com, O_RDWR|O_NOCTTY);
if (-1 == fd){
return(-1);
}
//if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口 Flag */
//{
// printf("fcntl failed!\n");
// return -1;
//}
return fd;
}
第5行:打开串口设备。
3. set_opt函数分析
代码如下:
/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
struct termios newtio,oldtio;
if ( tcgetattr( fd,&oldtio) != 0) {
perror("SetupSerial 1");
return -1;
}
bzero( &newtio, sizeof( newtio ) );
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
newtio.c_oflag &= ~OPOST; /*Output*/
switch( nBits )
{
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] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
newtio.c_cc[VTIME] = 0; /* 等待第 1 个数据的时间:
* 比如 VMIN 设为 10 表示至少读到 10 个数据才返回,
* 但是没有数据总不能一直等吧? 可以设置 VTIME(单位是 10 秒)
* 假设 VTIME=1,表示:
* 10 秒内一个数据都没有的话就返回
* 如果 10 秒内至少读到了 1 个字节,那就继续等待,完全读到
VMIN 个数据再返回
*/
tcflush(fd,TCIFLUSH);
if((tcsetattr(fd,TCSANOW,&newtio))!=0)
{
perror("com set error");
return -1;
}
//printf("set done!\n");
return 0;
}
4.2.3 上机实验
在Ubuntu上执行如下命令进行编译:
$ source /opt/remi-sdk/environment-setup-aarch64-poky-linux
$ $CC -o serial_send_recv serial_send_recv.c
$ scp serial_send_recv root@192.168.5.9:/home/root
在开发板上执行如下命令,然后输入“1”回车、输入“2”回车:
# ./serial_send_recv /dev/ttySC4
Enter a char: 1
get: 31 1
get: 0a
2
get: 32 2
get: 0a
上面的“get:311”表示读到了一个字符,它的16进制数值是 0x31,字符为“1”
4.3 普通串口应用编程(GPS模块实验)
4.3.1 GPS简介
全球定位系统(Global Positioning System,GPS)是一种以空中卫星为基础的高精度无线电导航的定位系统,它在全球任何地方以及近地空间都能够提供准确的地理位置、车行速度及精确的时间信息。GPS主要由三大组成部分:空间部分、地面监控部分和用户设备部分。GPS系统具有高精度、全天候、用广泛等特点。
太空卫星部分由多颗卫星组成,分成多个轨道,绕行地球一周约12小时。每个卫星均持续发射载有卫星轨道数据及时间的无线电波,提供地球上的各种接收机来应用。
地面管制部分,这是为了追踪及控制太空卫星运行所设置的地面管制站,主要工作为负责修正与维护每个卫星能够正常运转的各项参数数据,以确保每个卫星都能够提供正确的讯息给使用者接收机来接收。
使用者接收机(即用户设备),追踪所有的GPS卫星,并实时的计算出接收机所在位置的坐标、移动速度及时间。我们日常接触到的是用户设备部分,这里使用到的GPS模块即为用户设备接收机部分。
4.3.2 GPS模块硬件
GPS模块与外部控制器的通讯接口有多种方式,这里我们使用串口进行通讯,波特率为9600bps,1bit停止位,无校验位,无流控,默认每秒输出一次标准格式数据。
GPS模块外观如下图所示,通过排线与控制器进行供电和通讯。该模块为集成模块,没有相关原理图。
图4.2 GPS模块
GPS使用多种标准数据格式,目前最通用的GNSS格式是 NMEA0183格式。NMEA0183是最终定位格式,即将二进制定位格式转为统一标准定位格式,与卫星类型无关。这是一套定义接收机输出的标准信息,有几种不同的格式,每种都是独立相关的ASCII格式,逗点隔开数据流,数据流长度从30-100字符不等,通常以每秒间隔持续输出。
NVMEA0183格式主要针对民用定位导航,与专业 RTCM2.3/3.0和CMR+的GNSS数据格式不同。通过 NMEA0183格式,可以实现GNSS接收机与PC或PDA之间的数据交换,可以通过USB和COM口等通用数据接口进行数据传输,其兼容性高,数据传输稳定。这里我们使用串口进行是通讯,通信框图如下图所示。
图4.3 NVMEA0183格式
我们使用串口接收数据,收到的数据包含:$GPGGA(GPS定位数据)、$GPGLL(地理定位信息)、$GPGSA(当前卫星信息)、$GPGSV(可见卫星状态信息)、$GPRMC(推荐最小定位信息)、$GPVTG(地面速度信息)。
这里我们只分析$GPGGA(Global Positioning System Fix Data)即可,它包含了GPS定位经纬度、质量因子、HDOP、高程、参考站号等字段。其标准格式如下:
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh
$XXGGA语句各字段的含义和取值范围各字段的含义和取值范围见下表所示,XX取值有:
▪ GPGGA:单GPS
▪ BDGGA:单北斗
▪ GLGGA:单GLONASS
▪ GNGGA:多星联合定位
表4-2 $XXGGA字段
例子:$GPGGA,074529.82,2429.6717,N,11804.6973,E,1,8,1.098,42.110,,,M,,*76。
4.3.3 硬件连接
硬件连接原理图如下:
实物连接图如下:
图4.4 GPS模块硬件连接
4.3.4 程序解析本
节源码位于如下目录:
1. 程序主流程分析
在main函数了,打开串口、设置串口参数后,就直接读取 GPS原始数据、解析数据,代码如下:
while (1)
{
/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-
5.6,M,,*76"
*/ /* read line */
iRet = read_gps_raw_data(fd, buf);
/* parse line */
if (iRet == 0)
{
iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);
}
/* printf */
if (iRet == 0)
{
printf("Time : %s\n", time);
printf("ns : %s\n", ns);
printf("ew : %s\n", ew);
printf("Lat : %s\n", Lat);
printf("Lng : %s\n", Lng);
/* 纬度格式: ddmm.mmmm */
sscanf(Lat+2, "%f", &fLat);
fLat = fLat / 60;
fLat += (Lat[0] - '0')*10 + (Lat[1] - '0');
/* 经度格式: dddmm.mmmm */
sscanf(Lng+3, "%f", &fLng);
fLng = fLng / 60;
fLng += (Lng[0] - '0')*100 + (Lng[1] - '0')*10 + (Lng[2] - '0
');
printf("Lng,Lat: %.06f,%.06f\n", fLng, fLat);
}
}
第6行:读取GPS原始数据。
第11行:解析数据。
第15~33行:打印数据。
2. 读取GPS原始数据
代码如下:
int read_gps_raw_data(int fd, char *buf)
{
int i = 0;
int iRet;
char c;
int start = 0;
while (1)
{
iRet = read(fd, &c, 1);
if (iRet == 1)
{
if (c == '$')
start = 1;
if (start)
{
buf[i++] = c;
}
if (c == '\n' || c == '\r')
return 0;
}
else
{
return -1;
}
}
}
第10行:读取数据。
第13行:碰到“$”字符,表示新的一行数据开始了。
第19~20行:碰到回车换行,表示数据接收完毕。
3. 解析GPS数据
代码如下:
/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"
*/ int parse_gps_raw_data(char *buf, char *time, char *lat, char *ns, char *lng, cha
r *ew)
{
char tmp[10];
if (buf[0] != '$')
return -1;
else if (strncmp(buf+3, "GGA", 3) != 0)
return -1;
else if (strstr(buf, ",,,,,"))
{
printf("Place the GPS to open area\n");
return -1;
}
else {
//printf("raw data: %s\n", buf);
sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", tmp, time, lat, ns,
lng, ew);
return 0;
}
}
第7、9、11行:发现数据不对的话,返回错误。
第19行:解析数据。
4.3.5 上机实验
在Ubuntu上执行如下命令进行编译:
$ source /opt/remi-sdk/environment-setup-aarch64-poky-linux
$ $CC -o gps_read gps_read.c
$ scp gps_read root@192.168.5.9:/home/root
把模块放到开阔的地方,在开发板上执行如下命令:
# ./gps_read /dev/ttySC4
如您在使用瑞萨MCU/MPU产品中有任何问题,可识别下方二维码或复制网址到浏览器中打开,进入瑞萨技术论坛寻找答案或获取在线技术支持。
https://community-ja.renesas.com/zh/forums-groups/mcu-mpu/
未完待续
推荐阅读
即刻预约 | 瑞萨RZ/G通用MPU研讨会,现场送瑞米派!
VLP中添加客户的RZ/G2L板子编译
RZ/V2L ISP实现方式及功能简介