4、定义SoC系列与型号:编写soc.h、soc.c,定义CPU核心、中断控制器、系统时钟

好,咱们接着往下走。上一章我们把芯片的顶层架构搭起来了,Kconfig也配好了。现在要动真格的了——写soc.h和soc.c。

这两个文件,说白了就是告诉Zephyr:你这颗芯片的CPU是什么核、中断怎么管、时钟怎么跑。我刚开始移植的时候,觉得这步就是抄抄改改,结果踩了不少坑。嗯,咱们一个一个来拆解。

4.1 soc.h:芯片的“身份证”

soc.h是SoC层的头文件。它定义的东西,整个系统都会看到。我个人习惯把它分成三块:CPU核心信息、中断控制器定义、系统时钟相关宏。

先看一个我实际项目中用过的例子:

/* soc.h - 芯片ABC123系列定义 */

#ifndef _SOC_ABC123_H_
#define _SOC_ABC123_H_

/* CPU核心信息 */
#define SOC_CPU_CORTEX_M4          1
#define SOC_CPU_FPU_DOUBLE         1
#define SOC_CPU_MPU                1
#define SOC_CPU_NVIC_PRIO_BITS     4

/* 中断控制器 */
#define SOC_IRQ_NUM                240
#define SOC_IRQ_PRIO_LEVELS        16

/* 系统时钟 */
#define SOC_SYSCLK_HZ              160000000UL
#define SOC_SYSTICK_HZ             1000UL

/* 内存映射基址 */
#define SOC_FLASH_BASE             0x08000000
#define SOC_SRAM_BASE              0x20000000

#endif /* _SOC_ABC123_H_ */

这里有几个关键点,我跟你聊聊:

  • CPU核心宏:Zephyr用这些宏来开关对应的汇编优化、FPU支持、MPU配置。比如你用的是M4不带FPU,那就别定义SOC_CPU_FPU_DOUBLE,否则链接时会报错。
  • NVIC优先级位数:这个我吃过亏。有一次我写了个4,但芯片实际只支持3位,结果中断优先级乱套了。一定要查芯片参考手册的“NVIC Priority Bits”字段。
  • 系统时钟频率:SOC_SYSCLK_HZ是芯片的主频。注意,这里写的是最大支持频率,不是当前运行频率。当前频率在board层或者驱动里再配置。
注意:soc.h里不要放任何可变的东西。比如“当前PLL倍频系数”、“当前分频值”这些,应该放到board目录或者驱动层。soc.h只描述芯片的固有属性

4.2 soc.c:芯片的“初始化入口”

soc.c是SoC层的C文件。它的核心任务就一个:在系统启动早期,把芯片最基本的东西初始化好。

我一般会放三个函数:

  1. soc_init():早期初始化,比如关看门狗、设置系统时钟源。
  2. soc_interrupt_init():中断控制器初始化,比如设置向量表偏移。
  3. soc_clock_init():时钟树初始化,比如配置PLL、分频器。

来看一个实际代码:

/* soc.c - 芯片ABC123 SoC初始化 */

#include <soc.h>
#include <arch/arm/cortex_m/irq.h>

/* 早期初始化:关看门狗、设时钟源 */
void soc_init(void)
{
    /* 关闭独立看门狗(IWDG) */
    *((volatile uint32_t *)0x40003000) = 0x0000AAAA;
    *((volatile uint32_t *)0x40003000) = 0x00005555;
    *((volatile uint32_t *)0x40003004) = 0x00000000;

    /* 设置HSE为系统时钟源,8MHz外部晶振 */
    *((volatile uint32_t *)0x40023800) |= (1 << 16);
    while (!(*((volatile uint32_t *)0x40023800) & (1 << 17)));
}

/* 中断控制器初始化 */
void soc_interrupt_init(void)
{
    /* 设置向量表偏移到Flash起始地址 */
    __set_VTOR((uint32_t)0x08000000);

    /* 设置优先级分组:4位抢占优先级,0位子优先级 */
    NVIC_SetPriorityGrouping(3);
}

/* 系统时钟初始化 */
void soc_clock_init(void)
{
    /* 配置PLL:8MHz * 20 = 160MHz */
    *((volatile uint32_t *)0x40023804) = (20 << 0) | (1 << 22);

    /* 等待PLL就绪 */
    while (!(*((volatile uint32_t *)0x40023800) & (1 << 25)));

    /* 切换系统时钟到PLL */
    *((volatile uint32_t *)0x40023800) |= (2 << 0);
    while ((*((volatile uint32_t *)0x40023800) & (0x03 << 2)) != (2 << 2));
}

这里有个细节:soc_init()和soc_interrupt_init()的调用顺序。我建议先调soc_init()关看门狗,再调soc_interrupt_init()设中断。为什么?因为看门狗不关,你设中断的时候可能就被狗咬了。我曾经在一个项目里顺序搞反了,结果板子一直复位,查了两天才发现。

4.3 中断控制器的“潜规则”

Zephyr对ARM Cortex-M的中断控制器有标准支持。但不同芯片厂商会加一些“私货”。比如:

  • 有些芯片有额外的本地中断(如ST的EXTI)
  • 有些芯片支持中断嵌套分组(如NXP的LPC系列)
  • 有些芯片的向量表可以放在SRAM里

我个人习惯在soc.h里把这些“私货”也定义清楚:

/* 扩展中断控制器定义 */
#define SOC_EXTI_LINES             16
#define SOC_EXTI_IRQ_BASE          6

/* 中断优先级分组 */
#define SOC_NVIC_GROUPING          3   /* 4位抢占,0位子优先级 */

你想想看,如果不定义这些,后面写GPIO驱动的时候,中断号都不知道从哪算起。

4.4 系统时钟的“坑”与“解”

系统时钟这块,我遇到过最典型的问题就是:时钟源切换失败

比如你从HSE切到PLL,但PLL还没稳定,系统就卡死了。所以代码里一定要加超时处理。我一般这么写:

/* 带超时的PLL等待 */
uint32_t timeout = 1000000;
while (!(*((volatile uint32_t *)0x40023800) & (1 << 25))) {
    if (--timeout == 0) {
        /* 超时处理:回退到HSE */
        *((volatile uint32_t *)0x40023800) &= ~(3 << 0);
        break;
    }
}

另外,系统滴答时钟(SysTick)的配置也很关键。Zephyr默认用SysTick做系统时钟,频率一般是1000Hz。但有些芯片的SysTick时钟源可能不是系统时钟,而是外部时钟。这个要在soc.h里明确:

/* SysTick时钟源:0=外部时钟,1=系统时钟 */
#define SOC_SYSTICK_CLK_SRC        1

4.5 实战建议:先跑个“点灯”验证

写完soc.h和soc.c,我建议你立刻做一件事:编译一个最小系统,跑个GPIO点灯

为什么?因为soc层的代码直接影响系统启动。如果soc_init()里看门狗没关,或者时钟没配好,你后面写再多驱动都是白搭。

我自己的流程是这样的:

  1. 先写soc.h,定义好所有宏
  2. 写soc.c,只放soc_init()和soc_interrupt_init()
  3. 编译,看能不能通过
  4. 下载到板子,用逻辑分析仪看时钟输出
  5. 确认时钟正常后,再写soc_clock_init()

这样一步步来,出问题了也好定位。

小技巧:在soc_init()里加个GPIO翻转,用示波器看波形。如果波形出来了,说明芯片跑起来了。如果没波形,嗯...你懂的,回去查代码吧。

4.6 本章小结

soc.h和soc.c是芯片移植的“地基”。地基没打好,后面全是空中楼阁。记住三个核心:

  • soc.h:只放芯片固有属性,不放可变配置
  • soc.c:只做早期初始化,不做复杂逻辑
  • 中断和时钟:一定要查芯片手册,别想当然

下一章,咱们开始写board层的代码。到时候你会看到,board层怎么调用soc层提供的这些接口。嗯,那才是真正让芯片“活”起来的地方。