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层或者驱动里再配置。
4.2 soc.c:芯片的“初始化入口”
soc.c是SoC层的C文件。它的核心任务就一个:在系统启动早期,把芯片最基本的东西初始化好。
我一般会放三个函数:
- soc_init():早期初始化,比如关看门狗、设置系统时钟源。
- soc_interrupt_init():中断控制器初始化,比如设置向量表偏移。
- 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()里看门狗没关,或者时钟没配好,你后面写再多驱动都是白搭。
我自己的流程是这样的:
- 先写soc.h,定义好所有宏
- 写soc.c,只放soc_init()和soc_interrupt_init()
- 编译,看能不能通过
- 下载到板子,用逻辑分析仪看时钟输出
- 确认时钟正常后,再写soc_clock_init()
这样一步步来,出问题了也好定位。
4.6 本章小结
soc.h和soc.c是芯片移植的“地基”。地基没打好,后面全是空中楼阁。记住三个核心:
- soc.h:只放芯片固有属性,不放可变配置
- soc.c:只做早期初始化,不做复杂逻辑
- 中断和时钟:一定要查芯片手册,别想当然
下一章,咱们开始写board层的代码。到时候你会看到,board层怎么调用soc层提供的这些接口。嗯,那才是真正让芯片“活”起来的地方。