LinuxCNC学习(六)实时系统

Linux+RT-Preempt

Linux+RT-Preempt 是一个软实时解决方案,它通过preempt_rt补丁直接集成到 Linux 内核中,旨在减少内核中的不可抢占区域,允许更高优先级的任务打断低优先级的任务。这种方法不需要额外的实时核心,因而简化了系统架构,并提高了系统的通用性和兼容性。

对比测试

硬件:正点原子的STM32MP157开发板,本文仅用于个人学习测试,不当之处仅供参考。
使用cyclictest软件测试,对比抢占式内核的性能。
1、下载cyclictest源码,下载rt-tests-2.7.tar.gz
官网下载
2、编译cyclictest 交叉编译报错—numa.h找不到。
参考博客进行进行排除,cyclictest 交叉编译报错
通过官网下载numactl
下载numactl-2.0.16.tar.gz,解压后,在目录内创建build目录,执行命令

#启用交叉开发环境
source /opt/st/st32mp1/3.1-snapshot/eniroment-xxxx
./autogen.sh
./configure --host=arm --prefix=当前绝对路径/build
make
make install

3、将编译编译结果复制到交叉开发环境对应的文件系统中

#复制lib
sudo cp build/lib/* /opt/st/stm32mp1/3.1-snapshot/sysroot../cortex.../usr/lib
#复制头文件
sudo cp build/lib/* /opt/st/stm32mp1/3.1-snapshot/sysroot../cortex../usr/include

4、解压rt-tests-2.7.tar.gz,进入解压后的目录
首先打开Makefile文件,将CC=$(CROSS_COMPILE)gcc这行屏蔽掉
然后执行make即可编译出cyclictest软件
5、将cyclictest复制到开发板中,以下是默认系统的测试结果,CPU是STM32MP157,仅供参考。

// uname -a
Linux ATK-MP157 5.4.31-g886e225be #1 SMP PREEMPT Wed Feb 22 16:37:30 CST 2023 armv7l armv7l armv7l GNU/Linux
// 非实时核 单核测试
cyclictest -p 99 -i 1000 -l 10000
T: 0 (  962) P:99 I:1000 C:  10000 Min:     21 Act:   27 Avg:   36 Max:     981
// 非实时核 多核测试
cyclictest --mlockall --smp --priority=80 --interval=5000 --distance=0
T: 0 (  964) P:80 I:5000 C:  75039 Min:     14 Act:   17 Avg:   21 Max:     103
T: 1 (  ^Cin:     13 Act:   17 Avg:   28 Max:    1045 17 Avg:   28 Max:    1045
// 非实时核 多核测试
cyclictest --smp -p95 -m
T: 0 (  971) P:95 I:1000 C:  30102 Min:     13 Act:   15 Avg:   19 Max:     117
T^Croot@ATK-MP157:~# 500 C:  20043 Min:     13 Act:   29 Avg:   24 Max:    1045

导入preempt实时补丁

正点原子自带的内核版本是linux-5.4.31,在查找preempt实时补丁时没有找到对应版本,本次实验采用相近的linux-5.4.34版本的实时补丁。
1、下载Preempt补丁
官网下载Preempt实时补丁
下载patch-5.4.34-rt21.patch.gz,注意不要下载patchs开头的。
2、将补丁复制进内核源码的位置,执行打补丁命令

sudo path -p1 < path-5.4.34-rt21.patch.gz

3、打补丁执行完成后,启动make menuconfig配置内核

source /opt/st/st32mp1/3.1-snapshot/eniroment-xxxx
make menuconfig
#首页->General setup->Preempt module->full Preempt Kernel(RT) 选中,回车,保存退出
#启用交叉编译环境

#执行正点原子stm32mp157源码编译进入kernel执行
./build.sh
#将编译结果uImage复制到开发板/boot目录,重启

4、测试preempt
CPU是STM32MP157,仅供参考。

实时核
// uname -a
Linux ATK-MP157 5.4.31-rt21 #1 SMP PREEMPT_RT Sat Jul 27 20:42:31 CST 2024 armv7l armv7l armv7l GNU/Linux
cyclictest --mlockall --smp --priority=80 --interval=5000 --distance=0
T: 0 (  998) P:80 I:500 C:8314682 Min:     12 Act:   16 Avg:   17 Max:      75
T: 1 (  999) P:80 I:1000 C:4157291 Min:     13 Act:   17 Avg:   17 Max:      61
实时核
cyclictest --smp -p95 -m
T: 0 ( 1028) P:95 I:1000 C: 606791 Min:     12 Act:   18 Avg:   18 Max:      66
T: 1 ( 1029) P:95 I:1500 C: 404488 Min:     13 Act:   18 Avg:   17 Max:      60

在非实时核实时核同时运行cyclictest对比可以看到,线程响应的最大延迟有明显不同,非实时的最大延迟高达1045us,而实时核的最大延迟只有75us。

preempt应用示例

在Linux C应用程序中,通过pthread编程接口,传入合适的参数即可创建实时线程,由于preempt线程是Linux应用,和非实时线程通信时可以采用一般的Linux通信方式,包括共享内存、信号量、邮箱等。按官方文档说法,开发者不需要深入了解Preempt RT,而是使用标准C库开发实时应用。
详细的用户说明手册可以参考:官方手册
手册中提到,需要显示的调用pthread_attr_setstacksize函数为实时线程分配由mlockall申请的内存空间。设计实时线程的周期任务需要按以下的4个步骤进行:

  • 1.periodic_task_init():用于执行请求计时器、初始化变量、设置计时器周期等操作的初始化代码。
  • 2.do_rt_task():实时任务的内容
  • 3.wait_rest_of_period():实时任务完成后,等待剩余的时间。
  • 4.struct period_info 需要的结构体变量,用于1~2之间的变量传递。

特别的说明:实时线程任务优先采用clock_nanosleep()进行休眠,nanosleep()休眠函数使用的计时机制会被其他线程影响。实时线程中尽量不用动态内存申请,所用实时线程使用的内存空间都需要通过mlockall申请。同时要求使用preempt实时线程的应用,应首先启动非实时线程应用程序,然后分配合适的资源和调度参数予以开启实时线程。
以下是从官网复制的运行于STM32MP157CPU的preempt实时线程示例。

/*                                                                  
 * POSIX Real Time Example
 * using a single pthread as RT thread
 */
 
#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
struct period_info {
        struct timespec next_period;
        long period_ns;
};
 
static void inc_period(struct period_info *pinfo) 
{
        pinfo->next_period.tv_nsec += pinfo->period_ns;
 
        while (pinfo->next_period.tv_nsec >= 1000000000) {
                /* timespec nsec overflow */
                pinfo->next_period.tv_sec++;
                pinfo->next_period.tv_nsec -= 1000000000;
        }
}
 
static void periodic_task_init(struct period_info *pinfo)
{
        /* for simplicity, hardcoding a 1ms period */
        pinfo->period_ns = 1000000;
 
        clock_gettime(CLOCK_MONOTONIC, &(pinfo->next_period));
}
 
static void do_rt_task()
{
        /* Do RT stuff here. */
    printf("hei hei\n");
}
 
static void wait_rest_of_period(struct period_info *pinfo)
{
        inc_period(pinfo);
 
        /* for simplicity, ignoring possibilities of signal wakes */
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &pinfo->next_period, NULL);
}
void *thread_func(void *data)
{
        /* Do RT specific stuff here */
        struct period_info pinfo;
 
        periodic_task_init(&pinfo);
 
        while (1) {
                do_rt_task();
                wait_rest_of_period(&pinfo);
        }
 
        return NULL;
}
 
int main(int argc, char* argv[])
{
        struct sched_param param;
        pthread_attr_t attr;
        pthread_t thread;
        int ret;
 
        /* Lock memory */
        if(mlockall(MCL_CURRENT|MCL_FUTURE) == -1) {
                printf("mlockall failed: %m\n");
                exit(-2);
        }
 
        /* Initialize pthread attributes (default values) */
        ret = pthread_attr_init(&attr);
        if (ret) {
                printf("init pthread attributes failed\n");
                goto out;
        }
 
        /* Set a specific stack size  */
        ret = pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);
        if (ret) {
            printf("pthread setstacksize failed\n");
            goto out;
        }
 
        /* Set scheduler policy and priority of pthread */
        ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
        if (ret) {
                printf("pthread setschedpolicy failed\n");
                goto out;
        }
        param.sched_priority = 80;
        ret = pthread_attr_setschedparam(&attr, &param);
        if (ret) {
                printf("pthread setschedparam failed\n");
                goto out;
        }
        /* Use scheduling parameters of attr */
        ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
        if (ret) {
                printf("pthread setinheritsched failed\n");
                goto out;
        }
 
        /* Create a pthread with specified attributes */
        ret = pthread_create(&thread, &attr, thread_func, NULL);
        if (ret) {
                printf("create pthread failed\n");
                goto out;
        }
 
        /* Join the thread and wait until it is done */
        ret = pthread_join(thread, NULL);
        if (ret)
                printf("join pthread failed: %m\n");
 
out:
        return ret;
}

需要的CMakeListst.txt

#CMake最低版本要求
cmake_minimum_required(VERSION 3.5)

#项目名称
project(Preempt_test)

#设置交叉编译器
set(CMAKE_C_COMPILER arm-ostl-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER arm-ostl-linux-gnueabi-g++)
#设定输出
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

#添加源文件
aux_source_directory(. APP_SRC)

#生成目标程序
add_executable(preempt_1 ${APP_SRC})

#头文件
target_include_directories(preempt_1 PUBLIC $ {CMAKE_CURRENT_SOURCE_DIR})

#链接中间文件
target_link_libraries(preempt_1 pthread)

执行编译命令即可得到目标程序,复制到开发板即可运行。

#启动交叉开发环境
source /opt/st/st32mp1/3.1-snapshot/eniroment-xxxx
cmake .
make

Linux + RTAI 硬实时

根据查找的资料显示,主要应用于x86架构(最新仅支持到linux4.19),对于ARM的支持已经停止更新,略过。

Linux + xenomai 硬实时

网上有很多优秀的博客对xenomai进行了深入的分析,如:分析的博客原理分析的博客。按网上查找的资料普遍看法xenomai3更稳定,资料也全,本着学习的心态,尝试学习更新的xenomai4。官网网站

下载linux-evl

必须通过git克隆代码到本地,以便后续执行相关的git脚本命令,更新abi接口版本。

git clone https://source.denx.de/Xenomai/xenomai4/linux-evl.gi

当前目标开发板为正点原子STM32MP157开发板,而官方适配的内核版本是linux-5.4.31,通过查找发现xenom4的v5.4.31不是release版本,选择了相近的v5.4.32-evl2-release版本。在这里插入图片描述
按照正点原子的文档《【正点原子】STM32MP1嵌入式Linux驱动开发指南V2.0.pdf》的内核移植部分,从官网源码一步一步的移植,直到完成移植,得到一个可以运行于STM32MP157开发板的5.4.32的内核。
下载uImage到开发板,通过uname -a查看当前的系统版本
Linux ATK-MP157 5.4.32-gdcbb065b9 #1 SMP PREEMPT Sun Aug 25 22:40:40 CST 2024 armv7l armv7l armv7l GNU/Linux
第二步:配置EVL
最后执行make menuconfig
选择最外层的EVL real-time core按y
进入EVL real-time core选择
Enable quota-base scheduling 按y 启用周期时间调度策略
temporal partitioning policy 按y 时间划分策略
由上面自动生成,number of partitions 分区数 4
fixed sizes and limits 按 y 固定尺寸和限制
Debug support按y,也可不用
完成保存,编译源码。不要直接执行正点原子源码中的build.sh命令,根据build.sh内容逐次录入编译命令,以避免.config被重置。
第三步:复制编译结果
将uImage复制到开发板/boot内,将生成的modules.tar.bz2复制到开发板,解压并将其复制到/lib/modules/内。

下载libevl

必须通过git克隆代码到本地,以便后续执行相关的git脚本命令,更新abi接口版本。下载最新分支。

git clone https://source.denx.de/Xenomai/xenomai4/libevl.git

libevl非常依赖于linux-evl的abi版本,因此要找到当前使用的linux-evl支持的abi版本。首先将linux-evl源码的./scripts目录添加到系统环境变量PATH中

vi ~/.bashrc
#加入 export PATH=$PATH:绝对路径/linux-evl/scripts
#保存并source ~/.bashrc

进入到linux-evl目录内,执行git evlabi查询支持的abi版本。
查询支持的ABI版本
通过观察,当最高支持的ABI版本为21,需要将libevl的abi版本也切换到21才能编译libevl
第二步:切换libevl的ABI版本号
进入到libevl源码目录,执行git evlabi查询当前libevl所支持的ABI版本。
查询支持的ABI版本
当前libevl支持的最高版本是36,需要切换成目标的21版本。通过git回退命令,将21版本前的哈希值切换到21号版本。

git reset --hard 9b42b85

第三步:编译libevl
由于当前使用的版本较低,没有用meson构建,直接修改config.mk中关于交叉编译的项。关于高版本的meson构件,可参考后文。

#屏蔽原来的行
ARCH = arm
CROSS_COMPILE = arm-none-linux-gnueabihf-
UAPI ?= /home/hk/kernel/git_2_linux-evl/linux-evl
DESTDIR ?= /home/hk/kernel/git_2_linux-evl/libevl/output

确保arm-none-linux-gnueabihf-gcc可交叉编译STM32MP157的程序,直接make,然后make install。

  • 较高版本的libevl使用的meson进行构建。

对于使用meson编译,以下是一些建议
由于需要0.60.0以上的meson,首先sudo apt install python-pip,然后执行pip install --upgrade meson升级,没成功升级,按pip uninstall meson卸载,继续执行sudo apt-get remove --auto-remove meson,卸载干净。按pip install meson重新安装。通过which meson找到meson安装路径,将meson添加到~/.bashrc的PATH环境变量中,重启ubuntu,得到1.5.1版本。
确保能使用arm-none-linux-gnueabihf-gcc交叉编译STM32MP157程序。
$ meson setup [–cross-file 交叉编译的文件系统] [-Duapi=内核源码路径] [-Dbuildtype=构建的类型,调试,调试优化,发布版本] [-Dprefix=安装前缀] $buildir编译目标存放位置 $srcdir指定libevl源码目录位置
需要修改libevl源码目录内的./meson/arm-none-linux-gnueabihf末尾添加

[built-in options]
c_args = [‘-DCROSS=1’, ‘-I/home/hk/kernel/git_2_linux-evl/libevl/include’]

以下是示例编译代码
meson setup --cross-file /home/xx/kernel/git_2_linux-evl/libevl/meson/arm-none-linux-gnueabihf -Duapi=/home/xx/kernel/git_2_linux-evl/linux-evl
-Dbuildtype=release -Dprefix=/home/xx/kernel/git_2_linux-evl/libevl/output /home/xx/kernel/git_2_linux-evl/libevl/build /home/xx/kernel/git_2_linux-evl/libevl

进入build目录执行meson compile
最后执行ninja命令

第四步:复制编译结果到开发板
DESTDIR指定的位置内的lib/*全部复制到开发板/usr/lib目录中,将bin/*全部复制到开发板/usr/bin目录中,将libexec/*全部复制到开发板/usr/libexec目录中。将tests目录全部复制到开发板/usr目录内。
第五步:测试
官方测试命令说明 运行latmus -m测试系统的实时性。
实时性能测试
由于cyclictest程序使用了linux系统调用,在xenomai内核中,只要使用了linux系统调用就是最低优先级,因此直接运行cyclictest的测试结果和preempt_rt的差不多,改造cyclictest需要更多的时间,而xenomai也提供了配套的测试程序latmus。简单对比preempt_rt的MAX(60~75),和EVL的lat max(27~43),EVL的硬实时还是较优于preempt_rt的。

EVL应用示例

官方的文档提供了详细的内容,以下内容是个人的学习领悟,不当之处仅供参考,请以官方文档为准。官方文档
为了满足最后期限,双内核架构要求您的应用程序专门使用实时核心实现的专用系统调用接口。对于EVL核心,这个API是libevl,或者任何其他基于它的高级API,它遵守这个规则。在高优先级阶段运行时发出的任何常规Linux系统调用都会自动将调用者降级到低优先级阶段,在此过程中放弃实时保证。实时任务只能使用libevl提供的编程接口。
由pthread_create创建,创建EVL实时线程通信对象如(evl_create_mutex(), evl_create_event(), evl_create_xbuf()))最后通过evl_attach_self()绑定到EVL核心,实现实时任务线程的创建。再通过evl_detach_self()与EVL核心解绑,也可以不用显示调用,退出时自动与EVL核心解绑。所有EVL应用实时线程必须只能调用libevl提供的系统服务,一旦调用其他公共C库代码,将会引入延时抖动,(切换到了非实时核中运行)。可以将实时线程设计成阻塞式的调用方式,通过evl_wait_flags(), evl_get_sem(), evl_poll(), oob_read() 等待符合运行的条件。官方手册提供了一些指导是否允许EVL系统调用在普通Linux内调用。允许在EVL实时线程中用不会引起linux系统调用的函数,允许使用strcpy,memcpy等string.h中的部分函数,不允许使用引起malloc调用的函数,同理stdio.h的函数是不允许调用的。
编译应用程序时,只需要指定libevl/output/include和libevl/output/lib路径以及加-levl即可编译EVL的应用程序。即加上-I绝对路径libevl/output/include -L绝对路径libevl/output/lib -levl即可。
EVL还提供了一些用于实时线程和非实时线程的一些同步机制,包括事件标识组交叉缓冲区消息订阅文件代理。以及调度相关的使用说明:调度说明

示例程序

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sched.h>
#include <evl/sched.h>
#include <evl/thread.h>
#include <evl/timer.h>
#include <evl/clock.h>
#include <evl/proxy.h>

// 定义一个函数,作为线程执行的任务
void *print_string(void *ptr) {
    char *str = (char *)ptr; // 将void指针转换为字符指针
    printf("%s\n", str); // 打印传入的字符串
    while(1)
    {
        printf("%s\n", str); // 打印传入的字符串
        sleep(2);
    }
    return NULL; // 线程函数返回void指针
}
void timespec_add_ns(struct timespec *__restrict r,
	     		     const struct timespec *__restrict t,
			     long ns)
{
    long s, rem;

    s = ns / 1000000000;
    rem = ns - s * 1000000000;
    r->tv_sec = t->tv_sec + s;
    r->tv_nsec = t->tv_nsec + rem;
    if (r->tv_nsec >= 1000000000) {
            r->tv_sec++;
            r->tv_nsec -= 1000000000;
    }
}
void timer_delay(int tmfd, int ms)
{
    struct timespec now;
    struct itimerspec value, ovalue;
    int ret;
    int temp;
    /* Set up a 1 Hz periodic timer. */
    ret = evl_read_clock(EVL_CLOCK_MONOTONIC, &now);
    /* EVL always uses absolute timeouts, add 1s to the current date */
    if(ms < 1000)
    {
        timespec_add_ns(&value.it_value, &now, ms * 10000000);
        value.it_interval.tv_sec = 0;
        value.it_interval.tv_nsec = ms * 1000000;
    }
    else
    {
        temp = (ms / 1000) * 1000000000;
        temp += (ms % 1000 * 1000000);
        timespec_add_ns(&value.it_value, &now, temp);
        value.it_interval.tv_sec = ms / 1000;
        value.it_interval.tv_nsec = (ms % 1000) * 1000000;
    }
    ret = evl_set_timer(tmfd, &value, &ovalue);    
}
void *pthread_evl_task1(void *ptr)
{
    struct sched_param param;
    __u64 ticks;
  	param.sched_priority = 8;
    int tfd;
    int tmfd;
    int ret;
    char *str = (char *)ptr; // 将void指针转换为字符指针
    printf("%s\n", str); // 打印传入的字符串      
	ret = pthread_setschedparam(pthread_self(), SCHED_FIFO, &param); 
    tfd = evl_attach_self("evl-thread-task1:%d", getpid()); // 绑定到EVL核
    tmfd = evl_new_timer(EVL_CLOCK_MONOTONIC);
    timer_delay(tmfd, 500);
    for(;;)
    {
        /* Wait for the next tick to be notified. */
        ret = oob_read(tmfd, &ticks, sizeof(ticks)); 
        if (ticks > 1)
        {
            evl_printf("timer overrun! late tick num:%lld\n", ticks - 1);
            break;
        }     
        evl_printf("evl task 1...\r\n");  
    }
    timer_delay(tmfd, 0);
}
void *pthread_evl_task2(void *ptr)
{
    struct evl_sched_attrs attrs;
    int efd;
    int tfd;
    int tmfd;
    int ret;
    __u64 ticks;
    char *str = (char *)ptr; // 将void指针转换为字符指针
    printf("%s\n", str); // 打印传入的字符串  
	attrs.sched_policy = SCHED_FIFO;
	attrs.sched_priority = 7; /* [1-99] */
    tfd = evl_attach_self("evl-thread-task2:%d", getpid());     // 绑定到EVL核,私有属性,带/可默认恢复成PULIC属性  
    ret = evl_set_schedattr(tfd, &attrs);  
    tmfd = evl_new_timer(EVL_CLOCK_MONOTONIC); 
    timer_delay(tmfd, 1000);
    for(;;)
    {
        /* Wait for the next tick to be notified. */
        ret = oob_read(tmfd, &ticks, sizeof(ticks)); 
        if (ticks > 1)
        {
            evl_printf("timer overrun! late tick num:%lld\n", ticks - 1);
            break;
        }     
        evl_printf("evl task 2...\r\n");  
    }
    timer_delay(tmfd, 0);

}
int main() {
    pthread_t thread1, thread2; // 定义两个线程标识符
    pthread_t evl_thread1;
    pthread_t evl_thread2;
    
    // 创建第一个线程
    if (pthread_create(&thread1, NULL, &print_string, "Hello from Thread 1!")) {
        perror("Failed to create thread 1");
        return -1;
    }

    // 创建第二个线程
    if (pthread_create(&thread2, NULL, &print_string, "Hello from Thread 2!")) {
        perror("Failed to create thread 2");
        return -1;
    }

    // 创建一个EVL线程任务
 
    if (pthread_create(&evl_thread1, NULL, &pthread_evl_task1, "Hello from evl task")) {
        perror("Failed to create evl thread");
        return -1;
    }    
    // 创建第二个EVL线程任务
    if (pthread_create(&evl_thread2, NULL, &pthread_evl_task2, "Hello from evl task")) {
        perror("Failed to create evl thread");
        return -1;
    } 
    // 等待两个线程完成
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0; // 程序正常结束
}

#编译命令
arm-none-linux-gnueabihf-gcc -o evl_test -I/home/xx/kernel/git_2_linux-evl/libevl/output/include -L/home/xx/kernel/git_2_linux-evl/libevl/output/lib -lpthread -levl evl_test.c

复制到开发板,运行测试程序后,查看的状态
evl状态显示

总结

以上就是关于实时系统的相关学习,EVL实时线程还有关于EVL内核驱动的相关说明,参考xenomai官方文档。以上仅供参考。

为了学习 LinuxCNC 的源代码,您可以照以下步骤进行: 1. 下载源代码:您从 LinuxCNC 的官方网站或者代码托管平台(如 GitHub)上获取 LinuxCNC 的源代码。确保选择最新的稳定版本或者您感兴趣的特定版本。 2. 配置开发环境:在学习 LinuxCNC 源代码之前,您需要设置好开发环境。这包括安装编译器、构建工具和相关的依赖项。根据您的操作系统,可以参考 LinuxCNC 的官方文档或者社区资源来完成环境设置。 3. 理解代码结构:在开始阅读源代码之前,建议先对 LinuxCNC 的代码结构有一个大致的了解。查阅官方文档、阅读开发者指南或者参考社区资源,以便熟悉主要模块和文件。 4. 阅读关键文件:选择一些关键的文件进行阅读,这些文件包括主要的执行程序、核心模块和功能模块。开始阅读时,可以先从入口文件开始,然后根据代码中的引用关系逐步展开。 5. 调试和测试:学习源代码的过程中,可以通过调试和测试来加深对代码逻辑和功能的理解。尝试在开发环境中编译、运行和调试 LinuxCNC,并观察代码执行过程中的变化和结果。 6. 参与社区:LinuxCNC 拥有活跃的社区,您可以加入邮件列表、论坛或者参与开发者讨论,与其他开发者交流学习经验和解决问题。通过参与社区,您可以更好地理解 LinuxCNC 的设计思路和开发过程。 请注意,学习源代码是一个需要耐心和持续努力的过程。建议您在学习过程中保持良好的记录和整理习惯,以便后续查阅和复习。祝您在学习 LinuxCNC 源代码的过程中取得好的进展!如果您有任何进一步的问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值