硬件基础:SoC时钟树架构与常见外设总线
做嵌入式Linux驱动,说白了就是在跟两样东西打交道:时钟和总线。时钟是SoC的心跳,总线是外设的血管。这两样搞不明白,驱动写出来大概率是跑不起来的。
我记得刚入行那会儿,接手一个音频驱动的活儿。板子上的codec死活不出声,我查了三天寄存器,最后发现是I2C总线的时钟没配好——SCL波形都变形了。从那以后,我养成了一个习惯:拿到一块新板子,第一件事就是看时钟树。
2.1 SoC时钟树:一颗芯片的“心跳”网络
什么叫时钟树?说白了,就是一颗SoC内部所有时钟信号的产生、分配、变换的完整路径。你想想看,CPU要一个频率,DDR要另一个频率,USB又要一个频率——总不能每个模块都自己挂个晶振吧?
所以SoC的做法是:用少量几个晶振(通常是24MHz或32.768kHz),通过PLL倍频出高频时钟,再经过分频器、门控单元、多路选择器,分发到各个模块。
下面这张图是我自己画的一个典型SoC时钟树结构,你可以对照着看:
clocks属性和assigned-clock-rates。另外/sys/kernel/debug/clk/clk_summary这个文件能列出所有时钟的当前状态,调试时非常有用。
2.2 时钟管理的关键概念
做驱动开发,时钟管理这块有几个概念你必须烂熟于心:
| 概念 | 说明 | 驱动中的体现 |
|---|---|---|
| 时钟源 | 产生原始时钟信号的硬件单元(晶振、RC振荡器) | clk_get() 获取时钟句柄 |
| PLL | 锁相环,将低频时钟倍频到高频 | 设备树中配置 assigned-clock-rates |
| 分频器 | 将高频时钟降为模块需要的频率 | clk_set_rate() 动态调整 |
| 门控 | 开关时钟信号,用于省电 | clk_enable() / clk_disable() |
| 时钟域 | 同一时钟源驱动的逻辑单元集合 | 跨时钟域同步(CDC)问题 |
clk_prepare_enable(),但remove函数里只调了clk_disable(),忘了clk_unprepare()。结果模块卸载后,时钟门控没完全关掉,系统休眠时漏电严重。记住:prepare和enable是成对出现的,缺一不可。
2.3 三大外设总线:I2C、SPI、UART
搞嵌入式Linux,你迟早会跟这三条总线打交道。它们各有各的脾气,选对了事半功倍,选错了调试到哭。
2.3.1 I2C:两线制,多设备,慢但稳
I2C只有两根线:SCL(时钟)和SDA(数据)。所有设备挂在同一条总线上,靠地址区分。标准模式100kHz,快速模式400kHz,高速模式能到3.4MHz。
我个人习惯用I2C来挂那些配置类的外设——比如音频codec、传感器、触摸屏控制器。为什么?因为这类设备不需要高速数据传输,但要求连线少、支持多设备共享总线。
struct i2c_driver里的probe和remove,然后通过i2c_smbus_read/write系列函数读写寄存器。
举个例子,一个典型的I2C设备驱动probe函数长这样:
static int my_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_sensor_data *data;
int ret;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
i2c_set_clientdata(client, data);
/* 读取设备ID寄存器,确认硬件存在 */
ret = i2c_smbus_read_byte_data(client, REG_DEVICE_ID);
if (ret < 0) {
dev_err(&client->dev, "failed to read device id\n");
return ret;
}
dev_info(&client->dev, "sensor detected, id=0x%02x\n", ret);
return 0;
}
2.3.2 SPI:四线制,全双工,快
SPI用四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。全双工通信,速度轻松上几十MHz。
我一般在什么场景下用SPI?数据量大、实时性要求高的外设——比如LCD显示屏、ADC采样芯片、无线模块。SPI没有像I2C那样的地址概念,每个设备独占一条片选线,所以连线会多一些。
spi-cpol和spi-cpha配反了。嗯,这种低级错误,犯过一次就记住了。
SPI驱动在Linux里的注册方式:
static const struct spi_device_id my_spi_ids[] = {
{ "my_adc", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, my_spi_ids);
static struct spi_driver my_spi_driver = {
.driver = {
.name = "my_adc",
.of_match_table = my_spi_of_match,
},
.probe = my_spi_probe,
.remove = my_spi_remove,
.id_table = my_spi_ids,
};
module_spi_driver(my_spi_driver);
2.3.3 UART:异步串行,简单可靠
UART只需要两根数据线:TXD和RXD。没有时钟线,靠双方约定好的波特率来同步。常见的波特率有9600、115200、921600等。
UART最大的优点是简单。任何两个带UART的芯片,只要把TXD和RXD交叉一接,配好波特率,就能通信。我在项目中常用UART来做调试串口、GPS模块、蓝牙模块等。
2.4 三种总线的对比与选型
为了让你看得更清楚,我把三条总线的关键参数列了个表:
| 特性 | I2C | SPI | UART |
|---|---|---|---|
| 信号线数 | 2(SCL + SDA) | 4+(SCLK + MOSI + MISO + CS) | 2(TXD + RXD) |
| 通信方式 | 半双工 | 全双工 | 全双工 |
| 速度 | 100kHz ~ 3.4MHz | 10MHz ~ 100MHz+ | 最高约 1Mbps(典型) |
| 多设备支持 | 是(地址寻址) | 是(片选线) | 否(点对点) |
| 硬件流控 | 无 | 无 | 可选(RTS/CTS) |
| 典型应用 | 传感器、codec、EEPROM | LCD、ADC、无线模块 | 调试串口、GPS、蓝牙 |
选型时我的经验是:
- 设备多、连线少 → 选I2C
- 数据量大、速度快 → 选SPI
- 简单可靠、点对点 → 选UART