1 MYC-YG2UL Serial Port Introduction
The MYC-YG2UL platform supports multiple serial ports. The core board is configured with 5 serial ports by default, of which UART0, UART1, and UART2 have flow control (RTS and CTS signals) functions. Here, I will test the RS232 interface.
2 Serial Port Overview
With the development of embedded system applications, the application of Linux operating system is becoming more and more widespread. As a free and open source operating system, Linux has many unique advantages over Windows operating system. Linux can customize the kernel; Linux's GUI graphical interface can be selected arbitrarily; Linux can be remotely operated more conveniently and safely. With the continuous development and improvement of the Linux operating system, software development based on the Linux operating system has also made great progress and application. If Linux is introduced in the field of industrial control, it is inevitable to encounter the problem of how to implement serial communication under embedded Linux.
In the Linux operating system, operations on devices and files are equivalent to file operations, which greatly simplifies the system's operations on different devices and improves efficiency. In the program, devices and files are operated through file descriptors. A file descriptor is a non-negative index value that points to a file record table opened by each process in the kernel. When an existing file is opened or a new file is created, the kernel returns a file descriptor to the process. When a device needs to be read or written, the file descriptor also needs to be passed as a parameter to the corresponding function.
Linux device files are stored in the "/dev" directory. The device name corresponding to the serial port resource is "/dev/ttys+number", so the path of the device file corresponding to the serial port is "/dev/ttys*". In addition, the device name of the USB to serial port is usually "/dev/ttyUSB0". The operation method for devices in Linux is the same as the operation method for files.
3 Serial port settings detailed explanation
The serial port settings mainly involve setting the member values of the struct termios structure, as shown below:
#include <termios.h>
struct termios
{
unsigned short c_iflag; /* Input mode flag*/
unsigned short c_oflag; /* Output mode flag*/
unsigned short c_cflag; /* Control mode flag*/
unsigned short c_lflag; /* local mode flag*/
unsigned char c_line; /* line discipline*/
unsigned char c_cc[NCC]; /* Control characteristics*/
speed_t c_ispeed; /* Input speed*/
speed_t c_ospeed; /* output speed*/
};
termios is a standard interface defined in the Posix specification, which represents terminal devices (including virtual terminals, serial ports, etc.). Since the serial port is a terminal device, it is configured and controlled through the terminal programming interface. Therefore, before discussing serial port related programming in detail, you need to first understand the relevant knowledge of the terminal.
Terminal refers to the interface for users to communicate with computers, such as physical devices such as keyboards, monitors, and serial port devices, and virtual terminals on Windows. UNIX-like operating systems have text-based virtual terminals. You can use [Ctrl+Alt]+F1~F6 keys to enter a text-based virtual terminal. You can open more than dozens of graphical virtual terminals on the X Window. Virtual terminals on UNIX-like operating systems include xterm, rxvt, zterm, eterm, etc., while Windows has virtual terminals such as crt and putty.
The terminal has three working modes: canonical mode, non-canonical mode, and raw mode.
By setting the ICANNON flag in the c_lflag of the termios structure, you can define whether the terminal is working in canonical mode (set ICANNON flag) or non-canonical mode (clear ICANNON flag). The default is canonical mode.
In canonical mode, all input is processed on a line basis. Before the user enters a line terminator (carriage return, EOF, etc.), the system cannot read any characters entered by the user by calling the read() function. Line terminators (carriage return, etc.) other than EOF will be read into the buffer by the read() function like ordinary characters. In canonical mode, line editing is possible, and a call to the read() function can only read a line of data at most. If the number of bytes requested to be read in the read() function is less than the number of bytes that can be read in the current line, the read() function will only read the requested number of bytes, and the remaining bytes will be read next time.
In non-standard mode, all input is immediately valid, the user does not need to enter a line terminator, and line editing is not possible. In non-standard mode, the settings of the parameters MIN (c_cc[VMIN]) and TIME (c_cc[VTIME]) determine how the read() function is called. There are four different settings.
●MIN = 0 and TIME = 0: The read() function returns immediately. If there is readable data, the data is read and the number of bytes read is returned, otherwise the read fails and 0 is returned.
●MIN > 0 and TIME = 0: the read() function will be blocked until MIN bytes of data can be read.
●MIN = 0 and TIME > 0: As long as there is data to read or TIME tenths of a second has passed, the read() function will return immediately, and the return value is the number of bytes read. If the timeout expires and no data is read, the read() function returns 0.
MIN > 0 and TIME > 0: The read() function returns when there are MIN bytes to read or the time interval between two input characters exceeds TIME tenths of a second. Because the system starts the timer after the first character is entered, in this case, the read() function returns after reading at least one byte.
Strictly speaking, raw mode is a special non-standard mode. In raw mode, all input data is processed in bytes. In this mode, the terminal is not echoable, and all special terminal input/output control processing is unavailable. The terminal can be set to raw mode by calling the cfmakeraw() function, and the function can be implemented by the following code:
termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP| INLCR | IGNCR | ICRNL | IXON);
termios_p->c_oflag &= ~OPOST;
termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
termios_p->c_cflag &= ~(CSIZE | PARENB);
termios_p->c_cflag |= CS8;
Now let's explain the basic method of setting up the serial port. As mentioned above, the most basic operations of serial port settings include baud rate setting, parity bit and stop bit setting. The most important thing in this structure is c_cflag. By assigning values to it, users can set the baud rate, character size, data bit, stop bit, parity bit, hard and soft flow control, etc. In addition, c_iflag and c_cc are also commonly used flags. Here we mainly explain these three members in detail. The constant names supported by c_cflag are shown in the following table. The baud rate setting macro name is the corresponding baud rate value with B added in front. Due to the large number of values, this table does not list all of them.
Table 3-1 Constant names supported by c_cflag
CBAUD |
Baud rate bit mask |
B0 |
0 baud rate (discard DTR) |
… |
… |
B1800 |
1800 baud rate |
B2400 |
2400 baud rate |
B4800 |
4800 baud rate |
B9600 |
9600 baud rate |
B19200 |
19200 baud rate |
B38400 |
38400 baud rate |
B57600 |
57600 baud rate |
B115200 |
115200 baud rate |
EXTA |
External clock rate |
EXTB |
External clock rate |
CSIZE |
Bit mask of data bits |
CS5 |
5 data bits |
CS6 |
6 data bits |
CS7 |
7 data bits |
CS8 |
8 data bits |
CSTOPB |
2 stop bits (1 stop bit if not set) |
CREAD |
Receive Enable |
PARENB |
Check bit enable |
PARODD |
Use odd parity instead of even parity |
HUPCL |
Hang up when closing last (abandon DTR) |
CLOCAL |
Local connection (do not change port owner) |
CRTSCTS |
Hardware flow control |
Here, the c_cflag member cannot be initialized directly, but must be used through "and" and "or" operations to use some of the options.
The input mode flag c_iflag is used to control the character input processing of the port receiving end. The constant names supported by c_iflag are shown in the following table.
Table 3-2 Constant names supported by c_iflag
INPCK |
Parity Enable |
IGNPAR |
Ignore parity errors |
PARMRK |
Parity error mask |
ISTRIP |
Cut off the 8th bit |
IXON |
Enable output software flow control |
IXOFF |
Enable input software flow control |
INPCK |
Parity Enable |
IXANY |
Allows you to restart the output by entering any character (by default, the output is restarted by entering the starting character) |
IGNBRK |
Ignore input termination condition |
BRKINT |
Sends SIGINT signal when an input termination condition is detected |
INLCR |
Converts received NL (line feed) characters to CR (carriage return) characters |
IGNCR |
Ignore received CR (Carriage Return) characters |
ICRNL |
Converts received CR (carriage return) characters to NL (line feed) characters |
IUCLC |
Maps received uppercase characters to lowercase characters |
IMAXBEL |
Ring when input queue is full |
c_oflag is used to control the processing of characters sent out of the terminal port. The constant names supported by c_oflag are shown in Table 3. Because the speed of terminals is much faster than before, most delay masks are of little use.
Table 3-3 Constant names supported by c_oflag
OPOST |
Enable output processing. If this flag is not set, other flags are ignored. |
OLCUC |
Convert uppercase characters in output to lowercase |
ONLCR |
Convert newline characters ('\n') to carriage returns ('\r') in the output |
ONOCR |
If the current column number is 0, no carriage return character is output. |
OCRNL |
Convert carriage returns ('\r') to line feeds ('\n') in output |
ONLRET |
Do not output carriage return characters |
OFILL |
Send fill characters to provide delay |
OFDEL |
If this flag is set, the fill character is the DEL character, otherwise it is the NUL character. |
NLDLY |
Line break delay mask |
CRDLY |
Carriage return delay mask |
TABDLY |
Tab delay mask |
BSDLY |
Horizontal backspace delay mask |
VTDLY |
Vertical backspace delay mask |
FFLDY |
Form break delay mask |
c_lflag is used to control the local data processing and working mode of the terminal. The constant names supported by c_lflag are shown in the following table.
Table 3-4 Constant names supported by c_lflag
ISIG |
If a signal character (INTR, QUIT, etc.) is received, the corresponding signal will be generated |
ICANON |
Enable Canonical Mode |
ECHO |
Enable local echo function |
ECHOE |
If ICANON is set, backspace operation is allowed |
ECHOK |
If ICANON is set, the KILL character will delete the current line |
ECHONL |
If ICANON is set, newline characters can be echoed. |
ECHOCTL |
If ECHO is set, control characters (tab, line feed, etc.) will be displayed as "^X", where the ASCII code of X is equal to the ASCII code of the corresponding control character plus 0x40. For example, the backspace character (0x08) will be displayed as "^H" (the ASCII code of 'H' is 0x48) |
EC HOPRT |
If ICANON and IECHO are set, both the deleted characters (backspace, etc.) and the deleted characters will be displayed. |
ECHOKE |
If ICANON is set, the KILL character set in ECHOE and ECHOPRT is allowed to be echoed. |
NOFLSH |
Normally, when the INTR, QUIT, and SUSP control characters are received, the input and output queues are cleared. If this flag is set, all queues will not be cleared. |
TOSTOP |
If a background process attempts to write to its controlling terminal, the system sends a SIGTTOU signal to the process group of the background process. This signal usually terminates the execution of the process. |
IEXTEN |
Enable input processing |
c_cc defines special control characteristics. The constant names supported by c_cc are shown in the following table.
Table 3-5 Constant names supported by c_cc
VINTR |
Interrupt control character, the corresponding key is Ctrl+C |
VQUIT |
Exit operator, the corresponding key is Ctrl+Z |
VERASE |
Delete operator, the corresponding key is Backspace (BS) |
VKILL |
Delete line character, the corresponding key is Ctrl+U |
VEOF |
The file end character, the corresponding key is Ctrl+D |
VEOL |
Additional line ending character, corresponding key is Carriage return (CR) |
VEOL2 |
The second line end character, the corresponding key is Line feed (LF) |
VMIN |
Specifies the minimum number of characters to read |
VTIME |
Specifies the timeout between each character read |
The following is a detailed explanation of the basic process of setting serial port properties.
1. Save the original serial port configuration
First, for the sake of safety and convenience in debugging the program later, you can save the original serial port configuration. Here you can use the function tcgetattr(fd, &old_cfg). This function gets the configuration parameters of the terminal pointed to by fd and saves them in the termios structure variable old_cfg. This function can also test whether the configuration is correct and whether the serial port is available. If the call is successful, the function return value is 0, if the call fails, the function return value is -1. Its use is as follows:
if (tcgetattr(fd, &old_cfg) != 0)
{
perror("tcgetattr");
return -1;
}
2. Activate options
CLOCAL and CREAD are used for local connection and reception enable respectively, so these two options must be activated first by means of bit masks.
newtio.c_cflag |= CLOCAL | CREAD;
Calling the cfmakeraw() function can set the terminal to raw mode. In the following examples, raw mode is used for serial port data communication.
cfmakeraw(&new_cfg);
3. Set the baud rate
There are special functions for setting the baud rate, and users cannot operate directly through bit masks. The main functions for setting the baud rate are cfsetispeed() and cfsetospeed(). The use of these two functions is very simple, as shown below:
cfsetispeed(&\&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);
The cfsetispeed() function sets the data input baud rate in the termios structure, while the cfsetospeed() function sets the data input baud rate in the termios structure. Generally speaking, the user needs to set the terminal's input and output baud rates to the same. These functions return 0 on success and -1 on failure.
4. Set the character size
Unlike setting the baud rate, there is no ready-made function to set the character size, and a bit mask is required. Generally, the bit mask in the data bit is removed first, and then reset as required, as shown below:
new_cfg.c_cflag &= ~CSIZE; /* Clear data bit settings using data bit mask */
new_cfg.c_cflag |= CS8;
5. Set the parity bit
To set the parity bit, two members in termios are needed: c_cflag and c_iflag. First, you need to activate the parity bit enable flag PARENB in c_cflag and confirm whether to perform a check. This will generate a check bit for the output data and perform a check on the input data. At the same time, you also need to activate the parity check enable (INPCK) for the input data in c_iflag. If odd parity is enabled, the code is as follows:
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
When even parity is enabled, the code is as follows:
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* Clear the even-odd parity flag, then configure to even parity*/
new_cfg.c_iflag |= INPCK;
6. Set the stop bit
Setting the stop bit is achieved by activating CSTOPB in c_cflag. If the stop bit is one bit, clear CSTOPB; if the stop bit is two, activate CSTOPB. The following are the codes for one and two stop bits respectively:
new_cfg.c_cflag &= ~CSTOPB; /* Set the stop bit to one bit */
new_cfg.c_cflag |= CSTOPB; /* Set the stop bit to two bits */
7. Set minimum characters and waiting time
If there is no special requirement for receiving characters and waiting time, it can be set to 0. In this case, the read() function returns immediately in any case, and the serial port operation will be set to non-blocking mode, as shown below:
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;
8. Clear the serial port buffer
After the serial port is reset, the current serial port device needs to be properly processed. At this time, the tcdrain(), tcflow(), tcflush() and other functions declared in <termios.h> can be called to process the data in the current serial port buffer. Their formats are as follows:
int tcdrain(int fd); /* Block the program until all data in the output buffer are sent*/
int tcflow(int fd, int action); /* Used to pause or restart output */
int tcflush(int fd, int queue_selector); /* Used to clear the input/output buffer*/
In this example, the tcflush() function is used. The processing method for data that has not been transmitted in the buffer or data that has been received but not yet read depends on the value of queue_selector, which may have the following values.
●TCIFLUSH: Clear the data that was received but not read.
●TCOFLUSH: Clear the output data that has not been successfully transmitted.
●TCIOFLUSH: includes the first two functions, that is, clearing the input/output data that has not been processed.
As in this example, the first method is used, of course, the TCIOFLUSH parameter can be used:
tcflush(fd, TCIFLUSH);
9. Activate the configuration
After completing all the serial port configurations, you need to activate the configurations and make them effective. The function used here is tcsetattr(), and its function prototype is:
tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
The termios_p parameter is a new configuration variable of the termios type.
The optional_actions parameter has the following three possible values.
●TCSANOW: Configuration changes take effect immediately.
●TCSADRAIN: Configuration changes take effect after all output written to fd has been transmitted.
●TCSAFLUSH: All input received but not read in will be discarded before the modification takes effect.
This function returns 0 if the call is successful, and -1 if it fails. The code is as follows:
if ((tcsetattr(fd, TCSANOW, &new_cfg)) != 0)
{
perror("tcsetattr");
return -1;
}
The complete function of serial port configuration is given below. For the versatility of the function, the commonly used options are usually listed in the function, which can greatly facilitate the debugging and use of users in the future. The setting function is as follows:
/*
* @Function name: set_com_config
* @Function: serial port setting function
*/
int set_com_config(int fd,int baud_rate,int data_bits, char parity, int stop_bits)
{
struct termios new_cfg;
int speed;
/* Save and test the existing serial port parameter settings. If the serial port number is wrong, there will be relevant error information*/
if (tcgetattr(fd, &new_cfg) != 0)
{
perror("tcgetattr save");
return -1;
}
//Modify the control mode to ensure that the program does not occupy the serial port
new_cfg.c_cflag |= CLOCAL;
//Modify the control mode to enable reading input data from the serial port
new_cfg.c_cflag |= CREAD;
new_cfg.c_oflag &= ~(ONLCR | OCRNL);
new_cfg.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
new_cfg.c_iflag &= ~(ICRNL | INLCR);
new_cfg.c_iflag &= ~(IXON | IXOFF | IXANY);
/* Set baud rate */
switch (baud_rate)
{
case 2400:
{
speed = B2400;
}
break;
case 4800:
{
speed = B4800;
}
break;
case 9600:
{
speed = B9600;
}
break;
case 19200:
{
speed = B19200;
}
break;
case 38400:
{
speed = B38400;
}
break;
default:
case 115200:
{
speed = B115200;
}
break;
}
cfsetispeed(&new_cfg, speed); //Input baud rate
cfsetospeed(&new_cfg, speed); //Output baud rate
switch (data_bits) /* set data bits */
{
case 7:
{
new_cfg.c_cflag |= CS7;
}
break;
default:
case 8:
{
new_cfg.c_cflag |= CS8;
}
break;
}
switch (parity) /* Set the parity bit */
{
default:
case 'n':
case 'N':
{
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;
}
break;
case 'o':
case 'O':
{
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
}
break;
case 'e':
case 'E':
{
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD;
new_cfg.c_iflag |= INPCK;
}
break;
case 's': /* as no parity */
case 'S':
{
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_cflag &= ~CSTOPB;
}
break;
}
switch (stop_bits) /* Set stop bits */
{
default:
case 1:
{
new_cfg.c_cflag &= ~CSTOPB;
}
break;
case 2:
{
new_cfg.c_cflag |= CSTOPB;
}
}
//Modify the output mode, output the original data
new_cfg.c_oflag &= ~OPOST;
new_cfg.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
new_cfg.c_lflag &= ~(ISIG | ICANON);
//Set the waiting time and minimum received characters
new_cfg.c_cc[VTIME] = 0; /* Read a character and wait 0*(0/10)s */
new_cfg.c_cc[VMIN] = 1; /* The minimum number of characters to read is 0 */
//If data overflow occurs, receive data, but do not read it again. Refresh the received data but do not read it.
tcflush(fd, TCIFLUSH); /* Process unreceived characters */
if ((tcsetattr(fd, TCSANOW, &new_cfg)) != 0) /* Activate new configuration */
{
perror("tcsetattr action");
return -1;
}
printf("serial set success\n");
return 0;
}
4 Detailed explanation of serial port usage
After configuring the relevant properties of the serial port, you can open and read and write to the serial port. The functions it uses are the same as the read and write functions of ordinary files, which are open(), write() and read(). The only difference between them is that the serial port is a terminal device, so there will be some differences in selecting the specific parameters of the function. In addition, some additional functions will be used here to test the connection status of the terminal device, etc. The following will explain them in detail.
4.1 Open the serial port
Opening a serial port is the same as opening a normal file, both use the open() function, as shown below:
fd = open( "/dev/ttyS0", O_RDWR|O_NOCTTY|O_NDELAY);
As you can see, in addition to the normal read and write parameters, there are two more parameters, O_NOCTTY and O_NDELAY.
The O_NOCTTY flag is used to inform the Linux system that this parameter will not make the open file the controlling terminal of this process. If this flag is not specified, any input (such as keyboard abort signals, etc.) will affect the user's process.
The O_NDELAY flag informs the Linux system that this program does not care about the state of the DCD signal line (whether the other end of the port is activated or stopped). If the user specifies this flag, the process will remain in sleep mode until the DCD signal line is activated.
Next, the serial port can be restored to a blocked state to wait for serial port data to be read in. This can be achieved using the fcntl() function, as shown below:
fcntl(fd, F_SETFL, 0);
Then you can test whether the open file descriptor is connected to a terminal device to further confirm whether the serial port is opened correctly, as shown below:
isatty(STDIN_FILENO);
The function returns 0 if successful and -1 if failed.
At this point, a serial port has been successfully opened. Next, you can perform read and write operations on this serial port.
4.2 Read and write serial port
Reading and writing serial port operations are the same as reading and writing ordinary files. Use the read() and write() functions as shown below:
write(fd, buff, strlen(buff));
read(fd, buff, BUFFER_SIZE);
The following two examples show two programs for serial port reading and writing, which use the open_port() and set_com_config() functions described above. The program for writing to the serial port will run on the host machine, and the program for reading the serial port will run on the target board.
The program to write the serial port is as follows.
/*com_writer.c*/
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include "uart_api.h"
int main(void)
{
int fd;
char buff[BUFFER_SIZE];
if((fd=open_port(TARGET_COM_PORT))<0) /*Open the serial port*/
{
perror("open_port");
return 1;
}
if(set_com_config(fd,115200,8,'N',1)<0) /*Configure the serial port*/
{
perror("set_com_config error");
return 1;
}
do
{
printf("Input some words(enter 'quit' to exit):");
memset(buff,0,BUFFER_SIZE);
if(fgets(buff,BUFFER_SIZE,stdin)==NULL)
{
perror("fgets");
break;
}
write(fd,buff,strlen(buff));
}while(strncmp(buff,"quit",4));
close(fd);
return 0;
}
The program to read the serial port is as follows:
/*com_reader.c*/
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include "uart_api.h"
int main(void)
{
int fd;
char buff[BUFFER_SIZE];
if((fd=open_port(TARGET_COM_PORT))<0)
{
perror("open_port");
return 1;
}
if(set_com_config(fd,115200,8,'N',1)<0) /*Configure the serial port*/
{
perror("set_com_config ");
return 1;
}
do
{
memset(buff,0,BUFFER_SIZE);
if(read(fd,buff,BUFFER_SIZE)>0)
{
printf("the receive words are:%s",buff);
}
}while(strncmp(buff,"quit",4));
close(fd);
return 0;
}
/*uart_api.h*/
#ifndef UART_API_H
#define UART_API_H
#include <errno.h>
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <termios.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>
#define BUFFER_SIZE 36
#define TARGET_COM_PORT "/dev/ttySC4"
int set_com_config(int fd,int baud_rate, int data_bits,char parity,int stop_bits);
int open_port(char *com_port);
int init_port(char *com_port);
#endif
5 Serial port test
The device node used here is ttySC4, which can connect to RS232 to communicate with PC.
Run the program for writing serial port on the development board, and run the program for reading serial port on the target board. The running results are shown below.
Serial port write data:
Serial port receives data: