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()退出。否则线程跑完就没了。
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或关中断访问。
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表示一开始就有信号。看场景选。
- 最大计数:别设太大,否则信号量堆积,内存会爆。
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位对齐)。
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线程独立运行,不受中断影响
嗯,今天就讲这么多。下一章咱们聊定时器和PWM驱动,那才是真正让电机转起来的关键。回去把代码跑一跑,有问题随时问我。