3. Zephyr内核基础:线程、中断、信号量、消息队列的基本概念与使用

各位同学,咱们今天聊点硬核的。Zephyr内核,说白了就是整个系统的骨架。你写再多应用代码,如果内核这块没吃透,跑起来迟早要翻车。我刚开始接触Zephyr时,就犯过这种错——线程优先级设错了,结果中断响应慢得像蜗牛,被现场工程师追着骂了三天。

嗯,咱们今天就把线程、中断、信号量、消息队列这四个核心概念掰开揉碎了讲。你想想看,工业控制里最怕什么?怕任务卡死、怕数据打架、怕中断响应不及时。搞懂这些,你就能避开90%的坑。

3.1 线程:你的任务调度员

线程是什么?说白了就是一段独立运行的代码。Zephyr里每个线程都有自己的栈空间和优先级。我习惯把线程想象成工厂里的工人——有的工人负责搬砖(低优先级),有的负责按急停按钮(高优先级)。

创建线程很简单,用K_THREAD_DEFINE宏就行。看个例子:

#define STACK_SIZE 1024
#define THREAD_PRIORITY 5

K_THREAD_STACK_DEFINE(my_stack_area, STACK_SIZE);
struct k_thread my_thread_data;

void my_thread_entry(void *arg1, void *arg2, void *arg3)
{
    while (1) {
        printk("线程正在运行...\n");
        k_sleep(K_MSEC(1000));
    }
}

void main(void)
{
    k_thread_create(&my_thread_data, my_stack_area,
                    STACK_SIZE,
                    my_thread_entry,
                    NULL, NULL, NULL,
                    THREAD_PRIORITY, 0, K_NO_WAIT);
}

这里要注意几点:

  • 栈大小:别抠门,给够。我见过有人给256字节,结果递归调用直接栈溢出,系统死得悄无声息。
  • 优先级:0是最高,数值越大优先级越低。工业控制里,紧急任务用0-3,普通任务用4-7。
  • 入口函数:必须是个死循环,或者调用k_thread_abort()退出。否则线程跑完就没了。
避坑指南:我曾经在项目里把两个线程的优先级设成一样,结果它们互相抢CPU,导致看门狗超时复位。记住:同优先级线程是时间片轮转,别指望它们能精确配合。

3.2 中断:系统的紧急通道

中断是Zephyr里响应最快的方式。外部事件一来,CPU立刻停下手里活,跑去处理中断。我常说,中断就像你家的门铃——不管你在吃饭还是睡觉,门铃响了你就得去开门。

Zephyr里注册中断用IRQ_CONNECT宏:

#define MY_IRQ_LINE 42
#define MY_IRQ_PRIORITY 2

void my_isr(const void *arg)
{
    /* 处理中断 */
    printk("中断触发!参数: %d\n", (int)arg);
}

void main(void)
{
    IRQ_CONNECT(MY_IRQ_LINE, MY_IRQ_PRIORITY,
                my_isr, (void *)0x1234, 0);
    irq_enable(MY_IRQ_LINE);
}

中断处理有几个铁律:

  • 快进快出:ISR里别做复杂计算,别调用可能阻塞的函数。我见过有人在ISR里调printk,结果串口缓冲区满了,直接卡死。
  • 优先级别乱设:中断优先级比线程高,但中断之间也有优先级。工业控制里,电机编码器中断优先级最高,按键中断可以低一些。
  • 共享数据要保护:ISR和线程共享的变量,记得用atomic_t或关中断访问。
我的经验:如果ISR里要做的事太多,我习惯用信号量或消息队列把工作“踢”给线程。ISR只负责发信号,线程负责干活。这样既保证了响应速度,又避免了ISR里出幺蛾子。

3.3 信号量:任务间的红绿灯

信号量,说白了就是个计数器。线程A等信号量,线程B给信号量。这玩意儿在工业控制里太常用了——比如一个线程采集数据,另一个线程处理数据,中间就用信号量同步。

Zephyr里信号量分两种:计数信号量和二值信号量。二值信号量其实就是计数为1的信号量,常用于互斥访问。

K_SEM_DEFINE(my_sem, 0, 1);  // 初始0,最大1

void producer_thread(void *arg1, void *arg2, void *arg3)
{
    while (1) {
        /* 生产数据 */
        k_sem_give(&my_sem);
        k_sleep(K_MSEC(500));
    }
}

void consumer_thread(void *arg1, void *arg2, void *arg3)
{
    while (1) {
        k_sem_take(&my_sem, K_FOREVER);
        /* 消费数据 */
        printk("收到信号!\n");
    }
}

使用信号量要注意:

  • 超时设置K_FOREVER会一直等,K_NO_WAIT不等直接返回。工业控制里,我建议用K_MSEC(100)这样的超时,防止死锁。
  • 初始化值:初始为0表示一开始没信号,初始为1表示一开始就有信号。看场景选。
  • 最大计数:别设太大,否则信号量堆积,内存会爆。
避坑指南:我曾经在ISR里调k_sem_take,结果系统直接panic。记住:ISR里只能调k_sem_give,不能调k_sem_take。因为ISR不能阻塞等待。

3.4 消息队列:线程间的快递员

消息队列比信号量高级一点。信号量只传一个“有信号”的标记,消息队列能传一整包数据。你想想看,工业控制里要传传感器数据、控制指令,用消息队列再合适不过。

Zephyr的消息队列支持固定大小的消息。创建和使用都很简单:

#define MSG_SIZE 32
#define MSG_COUNT 10

K_MSGQ_DEFINE(my_msgq, MSG_SIZE, MSG_COUNT, 4);

void sender_thread(void *arg1, void *arg2, void *arg3)
{
    char msg[MSG_SIZE] = "Hello from sender!";
    while (1) {
        k_msgq_put(&my_msgq, msg, K_FOREVER);
        k_sleep(K_MSEC(1000));
    }
}

void receiver_thread(void *arg1, void *arg2, void *arg3)
{
    char msg[MSG_SIZE];
    while (1) {
        k_msgq_get(&my_msgq, msg, K_FOREVER);
        printk("收到消息: %s\n", msg);
    }
}

消息队列的几个关键点:

  • 消息大小:固定大小,别太大也别太小。我习惯用64字节,够用又不浪费。
  • 队列深度:根据数据量来。工业控制里,我一般设16或32,太深了浪费内存,太浅了容易丢数据。
  • 对齐方式:最后一个参数是消息对齐,一般设4(32位对齐)或8(64位对齐)。
我的经验:消息队列和信号量经常搭配使用。比如ISR里收到中断,先发信号量唤醒线程,线程再用消息队列取数据。这样ISR里只做最轻量的事,线程负责繁重工作。

3.5 实战组合:一个完整的例子

咱们来个综合例子。假设你要做一个电机控制器:一个中断采集编码器数据,一个线程处理数据,另一个线程控制PWM输出。

K_SEM_DEFINE(data_ready_sem, 0, 1);
K_MSGQ_DEFINE(encoder_msgq, sizeof(int32_t), 16, 4);

/* 编码器中断 */
void encoder_isr(const void *arg)
{
    int32_t position = read_encoder();
    k_msgq_put(&encoder_msgq, &position, K_NO_WAIT);
    k_sem_give(&data_ready_sem);
}

/* 数据处理线程 */
void process_thread(void *arg1, void *arg2, void *arg3)
{
    int32_t pos;
    while (1) {
        k_sem_take(&data_ready_sem, K_FOREVER);
        while (k_msgq_get(&encoder_msgq, &pos, K_NO_WAIT) == 0) {
            /* 处理位置数据 */
            calculate_speed(pos);
        }
    }
}

/* PWM控制线程 */
void pwm_thread(void *arg1, void *arg2, void *arg3)
{
    while (1) {
        update_pwm_duty(get_speed());
        k_sleep(K_MSEC(10));
    }
}

这个例子展示了:

  • ISR只做最轻量的事:读数据、发消息、给信号
  • 处理线程用信号量等待,用消息队列取数据
  • PWM线程独立运行,不受中断影响
核心要点:线程、中断、信号量、消息队列是Zephyr内核的四大金刚。搞懂它们,你就能写出稳定可靠的工业控制程序。记住:中断要快,线程要稳,信号量要准,消息队列要够。

嗯,今天就讲这么多。下一章咱们聊定时器和PWM驱动,那才是真正让电机转起来的关键。回去把代码跑一跑,有问题随时问我。