To eliminate some of the shortcomings of the server, assembled from household components, recently developed a device that I want to share. Its detailed description, with the scheme and source codes, is in the first part .
In this part of the article, we will look at how to interact with the serial port from the kernel space (kernel space) and how to organize work with several subsystems of the device via RS232 in Linux.
The device includes the following subsystems:
watchdog
;The serial port, relatively speaking, is two endpoints (endpoint). In this case, they need at least four:
The task can be solved if commands are sent directly to the device, and the traffic from it is disassembled using the dispatcher. The idea, after some thought, took the following form:
The wrnd
background process (daemon) acts as a dispatcher here, its purpose is to filter traffic into three FIFO channels:
Also shown on the diagram:
/dev/watchdog
, which controls the watchdog timer;Commands are transmitted to the device in text form, in the format [C|W|R|N][0-99]:[1]
, where the first letter is the subsystem identifier, then the command number and an argument can be followed by a colon.
Basically the software is written in pure C, contains scripts in Bash and Makefile. The installer is designed for Gentoo, but if you prefer, you can easily adapt it to other distributions.
The source code for the project is available on the GitHub alexcustos / wrn-project in the wrnd directory. The code contains data processing from the sensor, which can be found at the link: ATtiny85: prototype of a wireless sensor .
Further more about all the components.
After the publication of the article, the wdt.fifo channel was added to the project, which is served by the wrnd
daemon and duplicates the device file /dev/watchdog
. Both options are suitable for working with a watchdog
daemon and are generally similar. Therefore, the information given below is still relevant, and it will be useful if you want to get acquainted with how the watchdog timer driver is arranged.
From a programming point of view, Linux drivers are fairly simple. Their development is simplified by the fact that many well-documented examples are available in the kernel source tree. Some difficulties can arise only when you try to compile and debug your plans. The fact is that familiar functions from the glibc library are not available in the kernel space, you can only work with the functions represented in the kernel. In addition, interactive debugging of the code is very difficult.
But in this case, it is not scary, because the task is extremely simple, implement the character device /dev/watchdog
( code in full: wrn_wdt.c ). The device is serviced by driver number 10 (miscdevice), so you first need to determine the appropriate data structure:
static struct miscdevice wrn_wdt_miscdev = { .minor = WATCHDOG_MINOR, // watchdog .name = "watchdog", // /dev .fops = &wrn_wdt_fops, // };
Then set the handlers:
static const struct file_operations wrn_wdt_fops = { .owner = THIS_MODULE, // .llseek = no_llseek, // .write = wrn_wdt_write, // .unlocked_ioctl = wrn_wdt_ioctl, // ioctl .open = wrn_wdt_open, // .release = wrn_wdt_release, // };
Here, unlocked_ioctl, this is the usual handler for accessing the device descriptor via ioctl (), only for some time now, the call has become non-blocking, therefore additional measures need to be taken for synchronization, if necessary.
Now we need to define handlers for the operations of loading and unloading the module:
module_init(wrn_wdt_init); // module_exit(wrn_wdt_exit); //
If necessary, you can add parameters to the module through module_param (), MODULE_PARM_DESC () and specify for the order:
MODULE_DESCRIPTION("..."); // MODULE_AUTHOR("..."); // MODULE_LICENSE("..."); //
You can view this information with the command:
modinfo [path]/[module].ko
At this step, the driver is almost ready, it remains to make it useful. This requires the ability to, at a minimum, send data to the serial port. You cannot do this directly, because, in the kernel space, there is no API for this. The option associated with the removal of the standard driver and the development of its own can be immediately excluded as ideologically incorrect. Therefore, there are two options:
I think it is already clear that I chose the first option, and I have no excuse for that. But seriously, the line discipline would be worth using to place the traffic controller in the kernel space. But, as already noted, programming and debugging in this space is not trivial, and, if possible, should be avoided, as well as I / O operations that can be done in user space.
But the main reason for the inappropriate development of such a driver is that it is impossible to achieve autonomy, as if it were a USB driver. Line discipline is just an interlayer, which requires code in user space to configure the port parameters and install the required line discipline.
Anyway, the choice is made and the main driver code is the following:
static int __init wrn_wdt_init(void) { int ret; // , wrnd filp_port = filp_open(serial_port, O_RDWR | O_NOCTTY | O_NDELAY, 0); if (IS_ERR(filp_port)) ... // else { ret = misc_register(&wrn_wdt_miscdev); // miscdevice if (ret == 0) wdt_timeout(); // } return ret; } static void wdt_enable(void) { ... // spin_lock(&wrn_wdt_lock); // ( ) // watchdog keep-alive interval, // WRN , getnstimeofday(&t); time_delta = (t.tv_sec - wdt_keep_alive_sent.tv_sec) * 1000; // sec to ms time_delta += (t.tv_nsec - wdt_keep_alive_sent.tv_nsec) / 1000000; // ns to ms if (time_delta >= WDT_MIN_KEEP_ALIVE_INTERVAL) { // cmd_keep_alive fs = get_fs(); // FS set_fs(get_ds()); // KERNEL_DS ( ) ret = vfs_write(filp_port, cmd_keep_alive, strlen(cmd_keep_alive), &pos); set_fs(fs); // FS if (ret != strlen(cmd_keep_alive)) ... // getnstimeofday(&wdt_keep_alive_sent); } spin_unlock(&wrn_wdt_lock); // ( ) } static long wrn_wdt_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { ... // switch (cmd) { case WDIOC_KEEPALIVE: wdt_enable(); ret = 0; break; case WDIOC_SETTIMEOUT: ret = get_user(t, (int *)arg); // ... // timeout = t; wdt_timeout(); wdt_enable(); /* */ case WDIOC_GETTIMEOUT: ret = put_user(timeout, (int *)arg); // break; ... // } return ret; }
Beyond the framework of the code snippet above, support for MAGICCLOSE remains. It is necessary because the driver disables the watchdog when the device file is closed. Therefore, it is important to recognize the non-staffing completion of the watchdog
daemon, at which the file will be closed by the system. In this case, the MAGICCLOSE mechanism helps ensure a reboot. Its support provides for the deactivation of the watchdog timer only when the device file is closed immediately after receiving a special character, usually V.
The driver is built using the make driver
using the Makefile , its part is responsible for this:
TARGET_WDT = wrn_wdt ifneq ($(KERNELRELEASE),) # , obj-m := $(TARGET_WDT).o else # make KERNEL := $(shell uname -r) # driver: $(MAKE) -C /lib/modules/$(KERNEL)/build M=$(PWD) # install: driver $(MAKE) -C /lib/modules/$(KERNEL)/build M=$(PWD) modules_install endif
In order for the module to be loaded at system startup after installation, you need to add a line to the /etc/conf.d/modules
file:
modules="wrn_wdt"
The manual module can be loaded with the commands: modprobe wrn_wdt
or insmod ./wrn_wdt.ko
; unload: modprobe -r wrn_wdt
or rmmod wrn_wdt
; make sure the module is loaded: lsmod | grep wrn_wdt
lsmod | grep wrn_wdt
.
When using the wdt.fifo channel as a device file for a watchdog
daemon, it is important to make sure that the dependencies are correctly set and the wrnd
daemon starts earlier and the watchdog
stops later. Otherwise, respectively, the FIFO channel may not yet be created or the timer will not be deactivated, which may lead to an unwanted reset.
The purpose of the wrnd
daemon is to sort the stream of binary data coming from the serial port and convert them to a convenient format for services.
The default serial port settings are optimized for text terminals, so you need to align them, and the mode of operation with the device ( full code: serialport.c ):
// termios / struct termios2 ttyopts; memset(&ttyopts, 0, sizeof ttyopts); // if (ioctl(fd, TCGETS2, &ttyopts) != 0) ... // // ttyopts.c_cflag &= ~CBAUD; ttyopts.c_cflag |= BOTHER; ttyopts.c_ispeed = speed; // unsigned int, B9600 ttyopts.c_ospeed = speed; ... // // ttyopts.c_cc[VMIN] = vmin; // ( ) ttyopts.c_cc[VTIME] = vtime; // if (ioctl(fd, TCSETS2, &ttyopts) != 0) ... //
Here it is important to choose the optimal values of VMIN and VTIME. If they are zero, the port will be polled without delay, consuming system resources unnecessarily. A non-zero VMIN value, if there is no data, can block the stream indefinitely.
In this case, the data is read by one byte in the main program stream. Blocking it for a long time is not good, so VMIN is always zero, and VTIME can be changed through parameters, by default it is 5 (maximum delay is 0.5 seconds).
Received bytes go to a fixed-size buffer. Upon receipt of the expected number of bytes, the buffer is converted to the corresponding data structure (struct). This method is good, but has features that must be kept in mind. The compiler optimizes data structures by adding spaces between fields, aligning them at will, usually to the word boundary. Since the data is sent between different platforms, inconsistencies are possible due to the different word size and the order of the bytes in it (endian).
To get rid of alignment, you need to declare the data structures packed. AtmelStudio projects are built by default with the -fpack-struct
key, so it’s enough to make sure that there are no warnings about the cancellation of this key. It is not desirable to collect a wrnd project with this key, since there is no task to save memory at the expense of data access speed. It is enough to specify the corresponding attribute where necessary, for example:
struct payload_header { ... // } __attribute__ ((__packed__));
The process is started in the background by the daemon
function:
if (arguments->daemonize && daemon(0, 0) < 0) ... //
As a result, a copy of the process (fork) is created with the new PID, which continues to work further, and the current process ends. The arguments of the function indicate that for the new process, you must set the root directory as working and redirect the standard input, output and error streams to /dev/null
.
To prevent the daemon from shutting down when attempting to write to a FIFO to which the reader is not connected, you must ignore the SIGPIPE signal:
signal(SIGPIPE, SIG_IGN);
The purpose of the rest of the code, it is possible to list the following list:
logrotate
;Data enters the channel on a straight line in binary form at a speed of approximately 636 bytes / sec. To add entropy to /dev/random
use the rngd
daemon. In order to use only the necessary source, it needs to pass the parameters " --no-tpm=1 --no-drng=1 --rng-device /run/wrnd/rng.fifo
".
It is worth noting that in order to solve the problem of lack of entropy, it is not necessary to use a true random number generator. It is enough to run rngd
with the parameter " --rng-device /dev/urandom
". The algorithms used in /dev/urandom
quite good and the recommendations for not doing this are usually not completely justified. The results of comparative testing can be viewed in the first part , towards the end of the publication.
My choice in favor of a true random number generator is simple - I wanted to assemble such a device, and I did not find any arguments against it.
The data from the sensors are processed and then sent to the channel in the form of SQL queries to insert records into the table. The wrnsensors.sh example shows how to work with a SQLite3 database, but the INSERT query is universal and must go to any SQL database.
The channel is used by the management utility wrnctrl
, about it just below.
wrnd
can wrnd
using the make daemon
with the Makefile . To build a debug version, a debug target is provided.
The wrnctrl wrnctrl
requires a running wrnd
daemon, since it receives data from the device from the cmd.fifo channel.
Open at the same time FIFO with a predictable result, you can only write, while the reader will receive data from all sources. If several readers open one FIFO, then it is impossible to predict which of them will receive the data. This behavior is true by definition, but not desirable, therefore you need to synchronize access to cmd.fifo.
You can declare a file in Linux using flock
, and thus exclude simultaneous work with it, but only in your code. Since this mechanism does not work for named pipes (pipe), it is necessary to use an additional file in /tmp
( full code: wrnctrl ):
function device_cmd() { cmd=$1 # # (subshell), 4 ( # 4, if ! flock -w ${FIFO_MAX_WAIT} 4; then return; fi # # , if [ -O ${LOCK_NAME} ]; then chmod 666 ${LOCK_NAME}; fi # cmd.fifo 3, exec 3< <(timeout ${FIFO_MAX_LOCK} cat ${WRND_CMDFIFO}) sleep 0.2 # if [ -r /dev/fd/3 ]; then echo "$cmd" >${WRND_DEVICE} # # , FIFO timeout while read log_line <&3; do echo "$log_line" # done exec 3>&- # 3 fi ) 4>${LOCK_NAME} # 4 subshell, }
For flashing the device, the wrnctrl flash [firmware].hex
. Before its launch, you must stop the watchdog
and wrnd
. The command uses the avrdude
utility, you can install it through the package manager, for example:
emerge -av dev-embedded/avrdude
In addition to the files mentioned above, the project installation package also includes:
The project is assembled and installed using the command make install
. The installation should run as root. The files are copied by the install
system utility, which allows you to immediately set the rights and owner on the target files.
The USB interface for this device would certainly be preferable. But the lack of an available USB port in my server led to the appearance of the project in this form. Nevertheless, it turned out to be a fairly simple and stable device that I can recommend for playback.
Source: https://habr.com/ru/post/300968/
All Articles