原文地址:http://blog.csdn.net/yj4231/article/details/7751198
该系列文章将分为四个部分:
第一部分,将对SPI子系统整体进行描述,同时给出SPI的相关数据结构,最后描述SPI总线的注册。基于S3C2440的嵌入式Linux驱动——SPI子系统解读(一)
第二部分,即本篇文章,该文将对SPI的主控制器(master)驱动进行描述。
第三部分,该文将对SPI设备驱动,也称protocol 驱动,进行讲解。基于S3C2440的嵌入式Linux驱动——SPI子系统解读(三)
第四部分,通过SPI设备驱动留给用户层的API,我们将从上到下描述数据是如何通过SPI的protocol 驱动,由bitbang中转,最后由master驱动将数据传输出
去。 基于S3C2440的嵌入式Linux驱动——SPI子系统解读(四)
本文属于第二部分。
4. 主控制器驱动程序
4.1 定义 platform device
下列数据结构位于arch/arm/plat-s3c24XX/devs.c
-
/* SPI (0) */
-
-
static struct resource s3c_spi0_resource[] = {
-
[0] = {
-
.start = S3C24XX_PA_SPI,
-
.end = S3C24XX_PA_SPI + 0x1f,
-
.flags = IORESOURCE_MEM,
-
},
-
[1] = {
-
.start = IRQ_SPI0,
-
.end = IRQ_SPI0,
-
.flags = IORESOURCE_IRQ,
-
}
-
-
};
-
-
static u64 s3c_device_spi0_dmamask = 0xffffffffUL;
-
-
struct platform_device s3c_device_spi0 = {
-
.name = "s3c2410-spi",
-
.id = 0,
-
.num_resources = ARRAY_SIZE(s3c_spi0_resource),
-
.resource = s3c_spi0_resource,
-
.dev = {
-
.dma_mask = &s3c_device_spi0_dmamask,
-
.coherent_dma_mask = 0xffffffffUL
-
}
-
};
platform设备给出了spi0接口的寄存器地址资源以及IRQ资源。注意其设备名为s3c2410-spi。
4.2 定义platform driver
下列函数位于deivers/spi/s3c24xx.c。
-
MODULE_ALIAS("platform:s3c2410-spi");
-
static struct platform_driver s3c24xx_spi_driver = {
-
.remove = __exit_p(s3c24xx_spi_remove),
-
.suspend = s3c24xx_spi_suspend,
-
.resume = s3c24xx_spi_resume,
-
.driver = {
-
.name = "s3c2410-spi",
-
.owner = THIS_MODULE,
-
},
-
};
-
-
static int __init s3c24xx_spi_init(void)
-
{
-
return platform_driver_probe(&s3c24xx_spi_driver, s3c24xx_spi_probe);
-
}
-
-
static void __exit s3c24xx_spi_exit(void)
-
{
-
platform_driver_unregister(&s3c24xx_spi_driver);
-
}
-
-
module_init(s3c24xx_spi_init);
-
module_exit(s3c24xx_spi_exit);
调用了platform_driver_probe注册platform驱动,注册完成以后将会调用platform的s3c24xx_spi_probe函数。
NOTE:platform驱动的name和platform device的name是相同的。
4.2.1 s3c24xx_spi_probe函数
下列函数位于deivers/spi/s3c24xx.c。
-
static int __init s3c24xx_spi_probe(struct platform_device *pdev)
-
{
-
struct s3c2410_spi_info *pdata;
-
struct s3c24xx_spi *hw;
-
struct spi_master *master;
-
struct resource *res;
-
int err = 0;
-
-
-
master = spi_alloc_master(&pdev->dev, sizeof(struct s3c24xx_spi));
-
if (master == NULL) {
-
dev_err(&pdev->dev, "No memory for spi_master\n");
-
err = -ENOMEM;
-
goto err_nomem;
-
}
-
-
hw = spi_master_get_devdata(master);
-
memset(hw, 0, sizeof(struct s3c24xx_spi));
-
-
hw->master = spi_master_get(master);
-
hw->pdata = pdata = pdev->dev.platform_data;
-
hw->dev = &pdev->dev;
-
-
if (pdata == NULL) {
-
dev_err(&pdev->dev, "No platform data supplied\n");
-
err = -ENOENT;
-
goto err_no_pdata;
-
}
-
-
platform_set_drvdata(pdev, hw);
-
init_completion(&hw->done);
-
-
-
-
master->num_chipselect = hw->pdata->num_cs;
-
master->bus_num = pdata->bus_num;
-
-
-
-
hw->bitbang.master = hw->master;
-
hw->bitbang.setup_transfer = s3c24xx_spi_setupxfer;
-
hw->bitbang.chipselect = s3c24xx_spi_chipsel;
-
hw->bitbang.txrx_bufs = s3c24xx_spi_txrx;
-
hw->bitbang.master->setup = s3c24xx_spi_setup;
-
-
dev_dbg(hw->dev, "bitbang at %p\n", &hw->bitbang);
-
-
-
-
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
-
if (res == NULL) {
-
dev_err(&pdev->dev, "Cannot get IORESOURCE_MEM\n");
-
err = -ENOENT;
-
goto err_no_iores;
-
}
-
-
hw->ioarea = request_mem_region(res->start, (res->end - res->start)+1,
-
pdev->name);
-
-
if (hw->ioarea == NULL) {
-
dev_err(&pdev->dev, "Cannot reserve region\n");
-
err = -ENXIO;
-
goto err_no_iores;
-
}
-
-
hw->regs = ioremap(res->start, (res->end - res->start)+1);
-
if (hw->regs == NULL) {
-
dev_err(&pdev->dev, "Cannot map IO\n");
-
err = -ENXIO;
-
goto err_no_iomap;
-
}
-
-
hw->irq = platform_get_irq(pdev, 0);
-
if (hw->irq < 0) {
-
dev_err(&pdev->dev, "No IRQ specified\n");
-
err = -ENOENT;
-
goto err_no_irq;
-
}
-
-
err = request_irq(hw->irq, s3c24xx_spi_irq, 0, pdev->name, hw);
-
if (err) {
-
dev_err(&pdev->dev, "Cannot claim IRQ\n");
-
goto err_no_irq;
-
}
-
-
hw->clk = clk_get(&pdev->dev, "spi");
-
if (IS_ERR(hw->clk)) {
-
dev_err(&pdev->dev, "No clock for device\n");
-
err = PTR_ERR(hw->clk);
-
goto err_no_clk;
-
}
-
-
-
-
if (!pdata->set_cs) {
-
if (pdata->pin_cs < 0) {
-
dev_err(&pdev->dev, "No chipselect pin\n");
-
goto err_register;
-
}
-
-
err = gpio_request(pdata->pin_cs, dev_name(&pdev->dev));
-
if (err) {
-
dev_err(&pdev->dev, "Failed to get gpio for cs\n");
-
goto err_register;
-
}
-
-
hw->set_cs = s3c24xx_spi_gpiocs;
-
gpio_direction_output(pdata->pin_cs, 1);
-
} else
-
hw->set_cs = pdata->set_cs;
-
-
s3c24xx_spi_initialsetup(hw);
-
-
-
-
err = spi_bitbang_start(&hw->bitbang);
-
if (err) {
-
dev_err(&pdev->dev, "Failed to register SPI master\n");
-
goto err_register;
-
}
-
-
return 0;
-
-
err_register:
-
if (hw->set_cs == s3c24xx_spi_gpiocs)
-
gpio_free(pdata->pin_cs);
-
-
clk_disable(hw->clk);
-
clk_put(hw->clk);
-
-
err_no_clk:
-
free_irq(hw->irq, hw);
-
-
err_no_irq:
-
iounmap(hw->regs);
-
-
err_no_iomap:
-
release_resource(hw->ioarea);
-
kfree(hw->ioarea);
-
-
err_no_iores:
-
err_no_pdata:
-
spi_master_put(hw->master);;
-
-
err_nomem:
-
return err;
-
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
struct spi_master *spi_alloc_master(struct device *dev, unsigned size)
-
{
-
struct spi_master *master;
-
-
if (!dev)
-
return NULL;
-
-
master = kzalloc(size + sizeof *master, GFP_KERNEL);
-
if (!master)
-
return NULL;
-
-
device_initialize(&master->dev);
-
master->dev.class = &spi_master_class;
-
master->dev.parent = get_device(dev);
-
spi_master_set_devdata(master, &master[1]);
-
-
return master;
-
}
-
EXPORT_SYMBOL_GPL(spi_alloc_master);
该函数首先为spi_master结构体以及s3c24xx_spi结构体分配了空间,同时,spi_master.dev.driver_data指向了s3c24xx_spi。
s3c24xx_spi结构如下:
-
struct s3c24xx_spi {
-
-
struct spi_bitbang bitbang;
-
struct completion done;
-
-
void __iomem *regs;
-
int irq;
-
int len;
-
int count;
-
-
void (*set_cs)(struct s3c2410_spi_info *spi,
-
int cs, int pol);
-
-
-
const unsigned char *tx;
-
unsigned char *rx;
-
-
struct clk *clk;
-
struct resource *ioarea;
-
struct spi_master *master;
-
struct spi_device *curdev;
-
struct device *dev;
-
struct s3c2410_spi_info *pdata;
-
};
接着执行了该条语句:
hw->pdata = pdata = pdev->dev.platform_data; /*获取s3c2410_spi_info结构体指针*/
NOTE:在这里获取platform_device.dev.platform_data,也就是平台设备的相关数据,而在4.1小结中的arch/arm/plat-s3c24XX/devs.c文件中并没有发现platform_data的身影,因此这正式需要我们移植的地方。
随后初始化了completion,这个东东将用于实现同步I/O,详见下文。之后,为master定义了setup方法,为bitbang定义了3个方法。
接着获取了一系列的资源,同时注册了中断服务程序。接着调用s3c24xx_spi_initialsetup初始化控制器。我们来看下该函数。
该函数位于下列函数位于deivers/spi/s3c24xx.c。
-
static void s3c24xx_spi_initialsetup(struct s3c24xx_spi *hw)
-
{
-
-
-
clk_enable(hw->clk);
-
-
-
-
writeb(0xff, hw->regs + S3C2410_SPPRE);
-
writeb(SPPIN_DEFAULT, hw->regs + S3C2410_SPPIN);
-
writeb(SPCON_DEFAULT, hw->regs + S3C2410_SPCON);
-
-
if (hw->pdata) {
-
if (hw->set_cs == s3c24xx_spi_gpiocs)
-
gpio_direction_output(hw->pdata->pin_cs, 1);
-
*/
-
if (hw->pdata->gpio_setup)
-
hw->pdata->gpio_setup(hw->pdata, 1);
-
}
-
}
注意,这里设置了SPI0主控制器工作在master方式,使用中断模式。
最后调用了spi_bitbang_start函数,该函数非常重要,在下一小节中单独讲解。
4.2.2 spi_bitbang_start函数
下列函数位于drivers/spi/spi_bitbang.c
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
int spi_bitbang_start(struct spi_bitbang *bitbang)
-
{
-
int status;
-
-
if (!bitbang->master || !bitbang->chipselect)
-
return -EINVAL;
-
-
INIT_WORK(&bitbang->work, bitbang_work);
-
spin_lock_init(&bitbang->lock);
-
INIT_LIST_HEAD(&bitbang->queue);
-
-
if (!bitbang->master->transfer)
-
bitbang->master->transfer = spi_bitbang_transfer;
-
if (!bitbang->txrx_bufs) {
-
bitbang->use_dma = 0;
-
bitbang->txrx_bufs = spi_bitbang_bufs;
-
if (!bitbang->master->setup) {
-
if (!bitbang->setup_transfer)
-
bitbang->setup_transfer =
-
spi_bitbang_setup_transfer;
-
bitbang->master->setup = spi_bitbang_setup;
-
bitbang->master->cleanup = spi_bitbang_cleanup;
-
}
-
} else if (!bitbang->master->setup)
-
return -EINVAL;
-
-
-
bitbang->busy = 0;
-
bitbang->workqueue = create_singlethread_workqueue(
-
dev_name(bitbang->master->dev.parent));
-
if (bitbang->workqueue == NULL) {
-
status = -EBUSY;
-
goto err1;
-
}
-
-
-
-
-
status = spi_register_master(bitbang->master);
-
if (status < 0)
-
goto err2;
-
-
return status;
-
-
err2:
-
destroy_workqueue(bitbang->workqueue);
-
err1:
-
return status;
-
}
-
EXPORT_SYMBOL_GPL(spi_bitbang_start);
在这里,定义了控制器的transfer方法为spi_bitbang_transfer。创建了一个工作队列和一个工作bitbang_work,同时创建了一个链表。这些东东的作用将在后面介绍。
最后,调用了spi_register_master函数,该函数将完成SPI控制器的注册,其中还牵涉到spi_device的注册。因此该函数也非常重要。我们来看看这个函数
下列函数位于drivers/spi/spi_bitbang.c
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
int spi_register_master(struct spi_master *master)
-
{
-
static atomic_t dyn_bus_id = ATOMIC_INIT((1<<15) - 1);
-
struct device *dev = master->dev.parent;
-
int status = -ENODEV;
-
int dynamic = 0;
-
-
if (!dev)
-
return -ENODEV;
-
-
-
-
-
if (master->num_chipselect == 0)
-
return -EINVAL;
-
-
-
if (master->bus_num < 0) {
-
-
-
-
master->bus_num = atomic_dec_return(&dyn_bus_id);
-
dynamic = 1;
-
}
-
-
-
-
-
dev_set_name(&master->dev, "spi%u", master->bus_num);
-
status = device_add(&master->dev);
-
if (status < 0)
-
goto done;
-
dev_dbg(dev, "registered master %s%s\n", dev_name(&master->dev),
-
dynamic ? " (dynamic)" : "");
-
-
-
scan_boardinfo(master);
-
status = 0;
-
done:
-
return status;
-
}
-
EXPORT_SYMBOL_GPL(spi_register_master);
该函数中,执行了相关的检查,然后注册了master设备,随后调用了scan_boardinfo。函数如下:
下列函数位于drivers/spi/spi.c
-
-
-
-
-
static void scan_boardinfo(struct spi_master *master)
-
{
-
struct boardinfo *bi;
-
-
mutex_lock(&board_lock);
-
-
list_for_each_entry(bi, &board_list, list) {
-
struct spi_board_info *chip = bi->board_info;
-
unsigned n;
-
-
for (n = bi->n_board_info; n > 0; n--, chip++) {
-
if (chip->bus_num != master->bus_num)
-
continue;
-
-
-
-
-
(void) spi_new_device(master, chip);
-
}
-
}
-
mutex_unlock(&board_lock);
-
}
NOTE:这个函数通过boardinfo遍历的spi_board_info数组,而spi_board_info是在内核初始化过程中由spi_register_board_info进行注册的,在
linux/arch/arm/mach-s3c2440/mach-smdk2440.c中并没有调用过该函数,因此这也是需要移植的地方。
S3C2440共有两个接口:spi0和spi1。chip->bus_num表示该设备使用哪个spi接口,而master->bus_num正好表示了当前的接口。
该函数中,遍历spi_board_info,通过bus_num完成SPI设备和SPI控制器的匹配,匹配成功则开始建立spi_device设备,该过程通过调用spi_new_device实现。我们接着看下这个函数。
下列函数位于drivers/spi/spi.c
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
struct spi_device *spi_new_device(struct spi_master *master,
-
struct spi_board_info *chip)
-
{
-
struct spi_device *proxy;
-
int status;
-
-
-
-
-
-
-
-
-
proxy = spi_alloc_device(master);
-
if (!proxy)
-
return NULL;
-
-
WARN_ON(strlen(chip->modalias) >= sizeof(proxy->modalias));
-
-
proxy->chip_select = chip->chip_select;
-
proxy->max_speed_hz = chip->max_speed_hz;
-
proxy->mode = chip->mode;
-
proxy->irq = chip->irq;
-
strlcpy(proxy->modalias, chip->modalias, sizeof(proxy->modalias));
-
proxy->dev.platform_data = (void *) chip->platform_data;
-
proxy->controller_data = chip->controller_data;
-
proxy->controller_state = NULL;
-
-
status = spi_add_device(proxy);
-
if (status < 0) {
-
spi_dev_put(proxy);
-
return NULL;
-
}
-
-
return proxy;
-
}
-
EXPORT_SYMBOL_GPL(spi_new_device);
-
首先,创建了spi_device结构,让后通过板级信息spi_board_info将SPI从设备的相关信息复制给spi_device结构,从而完成了spi_device结构的定义,最后调用spi_add_device,完成spi_device的注册。
看下spi_add_device函数,该函数位于drivers/spi/spi.c
-
-
-
-
-
-
-
-
-
-
int spi_add_device(struct spi_device *spi)
-
{
-
static DEFINE_MUTEX(spi_add_lock);
-
struct device *dev = spi->master->dev.parent;
-
int status;
-
-
-
if (spi->chip_select >= spi->master->num_chipselect) {
-
dev_err(dev, "cs%d >= max %d\n",
-
spi->chip_select,
-
spi->master->num_chipselect);
-
return -EINVAL;
-
}
-
-
-
dev_set_name(&spi->dev, "%s.%u", dev_name(&spi->master->dev),
-
spi->chip_select);
-
-
-
-
-
-
-
mutex_lock(&spi_add_lock);
-
-
if (bus_find_device_by_name(&spi_bus_type, NULL, dev_name(&spi->dev))
-
!= NULL) {
-
dev_err(dev, "chipselect %d already in use\n",
-
spi->chip_select);
-
status = -EBUSY;
-
goto done;
-
}
-
-
-
-
-
-
status = spi->master->setup(spi);
-
if (status < 0) {
-
dev_err(dev, "can't %s %s, status %d\n",
-
"setup", dev_name(&spi->dev), status);
-
goto done;
-
}
-
-
-
status = device_add(&spi->dev);
-
if (status < 0)
-
dev_err(dev, "can't %s %s, status %d\n",
-
"add", dev_name(&spi->dev), status);
-
else
-
dev_dbg(dev, "registered child %s\n", dev_name(&spi->dev));
-
-
done:
-
mutex_unlock(&spi_add_lock);
-
return status;
-
}
-
EXPORT_SYMBOL_GPL(spi_add_device);
在注册spi_device之前,调用了master的setup方法,该方法又将调用s3c24xx_spi_setupxfer和s3c24xx_spi_chipsel函数。
s3c24xx_spi_setupxfer函数计算预分频系数并写入寄存器。
s3c24xx_spi_chipsel函数用于禁止或使能CS信号。当使能CS信号时,要设置控制寄存器。这里调用是禁止CS信号。
下列代码位于deivers/spi/s3c24xx.c。
-
#define SPCON_DEFAULT (S3C2410_SPCON_MSTR | S3C2410_SPCON_SMOD_INT)
-
#define SPPIN_DEFAULT (S3C2410_SPPIN_KEEP)
-
-
static inline struct s3c24xx_spi *to_hw(struct spi_device *sdev)
-
{
-
return spi_master_get_devdata(sdev->master);
-
}
-
-
static void s3c24xx_spi_gpiocs(struct s3c2410_spi_info *spi, int cs, int pol)
-
{
-
gpio_set_value(spi->pin_cs, pol);
-
}
-
-
static void s3c24xx_spi_chipsel(struct spi_device *spi, int value)
-
{
-
struct s3c24xx_spi *hw = to_hw(spi);
-
unsigned int cspol = spi->mode & SPI_CS_HIGH ? 1 : 0;
-
unsigned int spcon;
-
-
switch (value) {
-
case BITBANG_CS_INACTIVE:
-
hw->set_cs(hw->pdata, spi->chip_select, cspol^1);
-
break;
-
-
case BITBANG_CS_ACTIVE:
-
spcon = readb(hw->regs + S3C2410_SPCON);
-
-
-
if (spi->mode & SPI_CPHA)
-
spcon |= S3C2410_SPCON_CPHA_FMTB;
-
else
-
spcon &= ~S3C2410_SPCON_CPHA_FMTB;
-
-
if (spi->mode & SPI_CPOL)
-
spcon |= S3C2410_SPCON_CPOL_HIGH;
-
else
-
spcon &= ~S3C2410_SPCON_CPOL_HIGH;
-
-
spcon |= S3C2410_SPCON_ENSCK;
-
-
-
-
writeb(spcon, hw->regs + S3C2410_SPCON);
-
hw->set_cs(hw->pdata, spi->chip_select, cspol);
-
-
break;
-
}
-
}
-
-
static int s3c24xx_spi_setupxfer(struct spi_device *spi,
-
struct spi_transfer *t)
-
{
-
struct s3c24xx_spi *hw = to_hw(spi);
-
unsigned int bpw;
-
unsigned int hz;
-
unsigned int div;
-
-
-
bpw = t ? t->bits_per_word : spi->bits_per_word;
-
hz = t ? t->speed_hz : spi->max_speed_hz;
-
-
if (bpw != 8) {
-
dev_err(&spi->dev, "invalid bits-per-word (%d)\n", bpw);
-
return -EINVAL;
-
}
-
-
div = clk_get_rate(hw->clk) / hz;
-
-
-
-
-
div /= 2;
-
-
if (div > 0)
-
div -= 1;
-
-
if (div > 255)
-
div = 255;
-
-
dev_dbg(&spi->dev, "setting pre-scaler to %d (hz %d)\n", div, hz);
-
writeb(div, hw->regs + S3C2410_SPPRE);
-
-
spin_lock(&hw->bitbang.lock);
-
if (!hw->bitbang.busy) {
-
hw->bitbang.chipselect(spi, BITBANG_CS_INACTIVE);
-
-
}
-
spin_unlock(&hw->bitbang.lock);
-
-
return 0;
-
}
-
-
-
#define MODEBITS (SPI_CPOL | SPI_CPHA | SPI_CS_HIGH)
-
-
static int s3c24xx_spi_setup(struct spi_device *spi)
-
{
-
int ret;
-
-
if (!spi->bits_per_word)
-
spi->bits_per_word = 8;
-
-
if (spi->mode & ~MODEBITS) {
-
dev_dbg(&spi->dev, "setup: unsupported mode bits %x\n",
-
spi->mode & ~MODEBITS);
-
return -EINVAL;
-
}
-
-
ret = s3c24xx_spi_setupxfer(spi, NULL);
-
if (ret < 0) {
-
dev_err(&spi->dev, "setupxfer returned %d\n", ret);
-
return ret;
-
}
-
-
dev_dbg(&spi->dev, "%s: mode %d, %u bpw, %d hz\n",
-
__func__, spi->mode, spi->bits_per_word,
-
spi->max_speed_hz);
-
-
return 0;
-
}
至此,在probe函数中, 由spi_bitbang_start调用所引起的一系列函数调用都已讲解完毕。下面总结下整个调用过程:
可以看到,调用spi_bitbang_start以后,spi_master和spi_device都将被注册到内核中。
下面来看下platform driver的其他几个方法。
4.2.3 remove,suspend以及resume方法
-
static int __exit s3c24xx_spi_remove(struct platform_device *dev)
-
{
-
struct s3c24xx_spi *hw = platform_get_drvdata(dev);
-
-
platform_set_drvdata(dev, NULL);
-
-
spi_unregister_master(hw->master);
-
-
clk_disable(hw->clk);
-
clk_put(hw->clk);
-
-
free_irq(hw->irq, hw);
-
iounmap(hw->regs);
-
-
if (hw->set_cs == s3c24xx_spi_gpiocs)
-
gpio_free(hw->pdata->pin_cs);
-
-
release_resource(hw->ioarea);
-
kfree(hw->ioarea);
-
-
spi_master_put(hw->master);
-
return 0;
-
}
-
-
-
#ifdef CONFIG_PM /*如果定义了电源管理*/
-
-
static int s3c24xx_spi_suspend(struct platform_device *pdev, pm_message_t msg)
-
{
-
struct s3c24xx_spi *hw = platform_get_drvdata(pdev);
-
-
if (hw->pdata && hw->pdata->gpio_setup)
-
hw->pdata->gpio_setup(hw->pdata, 0);
-
-
clk_disable(hw->clk);
-
return 0;
-
}
-
-
static int s3c24xx_spi_resume(struct platform_device *pdev)
-
{
-
struct s3c24xx_spi *hw = platform_get_drvdata(pdev);
-
-
s3c24xx_spi_initialsetup(hw);
-
return 0;
-
}
-
-
#else
-
#define s3c24xx_spi_suspend NULL
-
#define s3c24xx_spi_resume NULL
-
#endif
至此,master 驱动的大体结构都已分析完毕,随后第三篇文章将介绍spi设备驱动。
阅读(1073) | 评论(0) | 转发(0) |