ioremap,你应该知道的事

因为现在使用是dts来表示板级,也就是machine,所以现在我们在内核使用内核映射使用的函数是of_iomap。

c代码:

 struct device_node *node = NULL;unsigned int irq_info[3] = { 0, 0, 0 };u32 phys_base;switch (idx) {case 0:node = of_find_node_by_name(NULL, "uart0");break;case 1:node = of_find_node_by_name(NULL, "uart1");break;case 2:node = of_find_node_by_name(NULL, "uart2");break;default:break;}if (node) {/* iomap registers */mtk_uart_default_settings[idx].uart_base = (unsigned long)of_iomap(node, 0);/* get IRQ ID */mtk_uart_default_settings[idx].irq_num = irq_of_parse_and_map(node, 0);}

dts代码:


uart0: serial@11002000 {compatible = "mediatek,mt8127-uart","mediatek,mt6577-uart";reg = <0 0x11002000 0 0x400>;interrupts = <GIC_SPI 51 IRQ_TYPE_LEVEL_LOW>;clocks = <&uart_clk>;status = "disabled";
};

重点

看到一篇写iomap非常不错的文章,转载分享给大家看看,这个文章写的比较久了,我觉得现在是有借鉴意义的。

转自:

https://blog.csdn.net/dinuliang/article/details/5823937

我们知道默认外设I/O资源是不在Linux内核空间中的(如sram或硬件接口寄存器等),若需要访问该外设I/O资源,必须先将其地址映射到内核空间中来,然后才能在内核空间中访问它。

Linux内核访问外设I/O内存资源的方式有两种:动态映射(ioremap)和静态映射(map_desc)。

一、动态映射(ioremap)方式

动态映射方式是大家使用了比较多的,也比较简单。即直接通过内核提供的ioremap函数动态创建一段外设I/O内存资源到内核虚拟地址的映射表,从而可以在内核空间中访问这段I/O资源。

Ioremap宏定义在asm/io.h内:

#define ioremap(cookie,size)           __ioremap(cookie,size,0)__ioremap函数原型为(arm/mm/ioremap.c):void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);
  • phys_addr:要映射的起始的IO地址

  • size:要映射的空间的大小

  • flags:要映射的IO空间和权限有关的标志

该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

举一个简单的例子: (取自s3c2410的iis音频驱动)

比如我们要访问s3c2410平台上的I2S寄存器, 查看datasheet 知道IIS物理地址为0x55000000,我们把它定义为宏S3C2410_PA_IIS,如下:

#define S3C2410_PA_IIS    (0x55000000)

若要在内核空间(iis驱动)中访问这段I/O寄存器(IIS)资源需要先建立到内核地址空间的映射:

our_card->regs = ioremap(S3C2410_PA_IIS, 0x100);if (our_card->regs == NULL) {err = -ENXIO;goto exit_err;}

创建好了之后,我们就可以通过readl(our_card->regs )或writel(value, our_card->regs)等IO接口函数去访问它。

二、静态映射(map_desc)方式

下面重点介绍静态映射方式即通过map_desc结构体静态创建I/O资源映射表。

内核提供了在系统启动时通过map_desc结构体静态创建I/O资源到内核地址空间的线性映射表(即page table)的方式,这种映射表是一种一一映射的关系。程序员可以自己定义该I/O内存资源映射后的虚拟地址。创建好了静态映射表,在内核或驱动中访问该I/O资源时则无需再进行ioreamp动态映射,可以直接通过映射后的I/O虚拟地址去访问它。

下面详细分析这种机制的原理并举例说明如何通过这种静态映射的方式访问外设I/O内存资源。

内核提供了一个重要的结构体struct machine_desc ,这个结构体在内核移植中起到相当重要的作用,内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。

machine_desc结构体的成员包含了体系架构相关部分的几个最重要的初始化函数,包括map_io, init_irq, init_machine以及phys_io , timer成员等。

machine_desc结构体定义如下:

struct machine_desc {/** Note! The first four elements are used* by assembler code in head-armv.S*/unsigned int        nr;        /* architecture number    */unsigned int        phys_io;    /* start of physical io    */unsigned int        io_pg_offst;    /* byte offset for io * page tabe entry    */const char        *name;        /* architecture name    */unsigned long        boot_params;    /* tagged list        */unsigned int        video_start;    /* start of video RAM    */unsigned int        video_end;    /* end of video RAM    */unsigned int        reserve_lp0 :1;    /* never has lp0    */unsigned int        reserve_lp1 :1;    /* never has lp1    */unsigned int        reserve_lp2 :1;    /* never has lp2    */unsigned int        soft_reboot :1;    /* soft reboot        */void            (*fixup)(struct machine_desc *,struct tag *, char **,struct meminfo *);void            (*map_io)(void);/* IO mapping function    */void            (*init_irq)(void);struct sys_timer    *timer;        /* system tick timer    */void            (*init_machine)(void);
};

这里的map_io成员即内核提供给用户的创建外设I/O资源到内核虚拟地址静态映射表的接口函数。Map_io成员函数会在系统初始化过程中被调用,流程如下:

Start_kernel -> setup_arch() --> paging_init() --> devicemaps_init()中被调用

Machine_desc结构体通过MACHINE_START宏来初始化。

注:MACHINE_START的使用及各个成员函数的调用过程请参考:

http://blog.chinaunix.net/u2/60011/showart_1010489.html

用户可以在定义Machine_desc结构体时指定Map_io的接口函数,这里以s3c2410平台为例。

s3c2410 machine_desc结构体定义如下:

/* arch/arm/mach-s3c2410/Mach-smdk2410.c */
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch* to SMDK2410 *//* Maintainer: Jonas Dietsche */.phys_io    = S3C2410_PA_UART,.io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,.boot_params    = S3C2410_SDRAM_PA + 0x100,.map_io        = smdk2410_map_io,.init_irq    = s3c24xx_init_irq,.init_machine    = smdk2410_init,.timer        = &s3c24xx_timer,
MACHINE_END

如上,map_io被初始化为smdk2410_map_io。smdk2410_map_io即我们自己定义的创建静态I/O映射表的函数。在Porting内核到新开发板时,这个函数需要我们自己实现。

(注:这个函数通常情况下可以实现得很简单,只要直接调用iotable_init创建映射表就行了,我们的板子内核就是。不过s3c2410平台这个函数实现得稍微有点复杂,主要是因为它将要创建IO映射表的资源分为了三个部分(smdk2410_iodesc, s3c_iodesc以及s3c2410_iodesc)在不同阶段分别创建。这里我们取其中一个部分进行分析,不影响对整个概念的理解。)

S3c2410平台的smdk2410_map_io函数最终会调用到s3c2410_map_io函数。

流程如下:

s3c2410_map_io -> s3c24xx_init_io -> s3c2410_map_io

下面分析一下s3c2410_map_io函数:


void __init s3c2410_map_io(struct map_desc *mach_desc, int mach_size)
{/* register our io-tables */iotable_init(s3c2410_iodesc, ARRAY_SIZE(s3c2410_iodesc));……
}

iotable_init内核提供,定义如下:


/** Create the architecture specific mappings*/
void __init iotable_init(struct map_desc *io_desc, int nr)
{int i;for (i = 0; i < nr; i++)create_mapping(io_desc + i);
}

由上知道,s3c2410_map_io最终调用iotable_init建立映射表。

iotable_init函数的参数有两个:一个是map_desc类型的结构体,另一个是该结构体的数量nr。这里最关键的就是struct map_desc。map_desc结构体定义如下:


/* include/asm-arm/mach/map.h */
struct map_desc {unsigned long virtual;    /* 映射后的虚拟地址 */unsigned long pfn;        /* I/O资源物理地址所在的页帧号 */unsigned long length;    /* I/O资源长度 */unsigned int type;        /* I/O资源类型 */
};

create_mapping函数就是通过map_desc提供的信息创建线性映射表的。

这样的话我们就知道了创建I/O映射表的大致流程为:只要定义相应I/O资源的map_desc结构体,并将该结构体传给iotable_init函数执行,就可以创建相应的I/O资源到内核虚拟地址空间的映射表了。

我们来看看s3c2410是怎么定义map_desc结构体的(即上面s3c2410_map_io函数内的s3c2410_iodesc)。


/* arch/arm/mach-s3c2410/s3c2410.c */
static struct map_desc s3c2410_iodesc[] __initdata = {IODESC_ENT(USBHOST),IODESC_ENT(CLKPWR),IODESC_ENT(LCD),IODESC_ENT(TIMER),IODESC_ENT(ADC),IODESC_ENT(WATCHDOG),
};

IODESC_ENT宏如下:

#define IODESC_ENT(x) { (unsigned long)S3C24XX_VA_##x, __phys_to_pfn(S3C24XX_PA_##x), S3C24XX_SZ_##x, MT_DEVICE }

展开后等价于:


static struct map_desc s3c2410_iodesc[] __initdata = {{.virtual    =     (unsigned long)S3C24XX_VA_ LCD),.pfn        =     __phys_to_pfn(S3C24XX_PA_ LCD),.length    =    S3C24XX_SZ_ LCD,.type    =     MT_DEVICE},……
};

S3C24XX_PA_ LCD和S3C24XX_VA_ LCD为定义在map.h内的LCD寄存器的物理地址和虚拟地址。在这里map_desc 结构体的virtual成员被初始化为S3C24XX_VA_ LCD,pfn成员值通过__phys_to_pfn内核函数计算,只需要传递给它该I/O资源的物理地址就行。Length为映射资源的大小。MT_DEVICE为I/O类型,通常定义为MT_DEVICE。

这里最重要的即virtual 成员的值S3C24XX_VA_ LCD,这个值即该I/O资源映射后的内核虚拟地址,创建映射表成功后,便可以在内核或驱动中直接通过该虚拟地址访问这个I/O资源。

 S3C24XX_VA_ LCD以及S3C24XX_PA_ LCD定义如下:/* include/asm-arm/arch-s3c2410/map.h *//* LCD controller */#define S3C24XX_VA_LCD          S3C2410_ADDR(0x00600000)   //LCD映射后的虚拟地址#define S3C2410_PA_LCD           (0x4D000000)    //LCD寄存器物理地址#define S3C24XX_SZ_LCD           SZ_1M        //LCD寄存器大小

S3C2410_ADDR 定义如下:

#define S3C2410_ADDR(x)        ((void __iomem *)0xF0000000 + (x))

这里就是一种线性偏移关系,即s3c2410创建的I/O静态映射表会被映射到0xF0000000之后。(这个线性偏移值可以改,也可以你自己在virtual成员里手动定义一个值,只要不和其他IO资源映射地址冲突,但最好是在0XF0000000之后。)

(注:其实这里S3C2410_ADDR的线性偏移只是s3c2410平台的一种做法,很多其他ARM平台采用了通用的IO_ADDRESS宏来计算物理地址到虚拟地址之前的偏移。

IO_ADDRESS宏定义如下:

/* include/asm/arch-versatile/hardware.h *//* macro to get at IO space when running virtually */#define IO_ADDRESS(x)            (((x) & 0x0fffffff) + (((x) >> 4) & 0x0f000000) + 0xf0000000) )

s3c2410_iodesc这个映射表建立成功后,我们在内核中便可以直接通过S3C24XX_VA_ LCD访问LCD的寄存器资源。

如:S3c2410 lcd驱动的probe函数内

 /* Stop the video and unset ENVID if set */
info->regs.lcdcon1 &= ~S3C2410_LCDCON1_ENVID;
lcdcon1 = readl(S3C2410_LCDCON1); //read映射后的寄存器虚拟地址
writel(lcdcon1 & ~S3C2410_LCDCON1_ENVID, S3C2410_LCDCON1); //write映射后的虚拟地址

S3C2410_LCDCON1寄存器地址为相对于S3C24XX_VA_LCD偏移的一个地址,定义如下:

/* include/asm/arch-s3c2410/regs-lcd.h */#define S3C2410_LCDREG(x) ((x) + S3C24XX_VA_LCD)/* LCD control registers */#define S3C2410_LCDCON1        S3C2410_LCDREG(0x00)

到此,我们知道了通过map_desc结构体创建I/O内存资源静态映射表的原理了。总结一下发现其实过程很简单,一通过定义map_desc结构体创建静态映射表,二在内核中通过创建映射后虚拟地址访问该IO资源。

三、I/O静态映射方式应用实例

I/O静态映射方式通常是用在寄存器资源的映射上,这样在编写内核代码或驱动时就不需要再进行ioremap,直接使用映射后的内核虚拟地址访问。同样的IO资源只需要在内核初始化过程中映射一次,以后就可以一直使用。

寄存器资源映射的例子上面讲原理时已经介绍得很清楚了,这里我举一个SRAM的实例介绍如何应用这种I/O静态映射方式。当然原理和操作过程同寄存器资源是一样的,可以把SRAM看成是大号的I/O寄存器资源。

比如我的板子在0x30000000位置有一块64KB大小的SRAM。我们现在需要通过静态映射的方式去访问该SRAM。我们要做的事内容包括修改kernel代码,添加SRAM资源相应的map_desc结构,创建sram到内核地址空间的静态映射表。写一个Sram Module,在Sram Module 内直接通过静态映射后的内核虚拟地址访问该sram。

第一步:创建SRAM静态映射表

在我板子的map_des结构体数组(xxx_io_desc)内添加SRAM资源相应的map_desc。如下:

static struct map_desc xxx_io_desc[] __initdata = {{.virtual    = IO_ADDRESS(XXX _UART2_BASE),.pfn        = __phys_to_pfn(XXX _UART2_BASE),.length        = SZ_4K,.type        = MT_DEVICE},{.virtual    = IO_ADDRESS(XXX_SRAM_BASE),.pfn        = __phys_to_pfn(XXX_SRAM_BASE),.length        = SZ_4K,.type        = MT_DEVICE},
};

宏XXX_SRAM_BASE为我板子上SRAM的物理地址,定义为0x30000000。我的kernel是通过IO_ADDRESS的方式计算内核虚拟地址的,这点和之前介绍的S3c2410有点不一样。不过原理都是相同的,为一个线性偏移, 范围在0xF0000000之后。

第二步:写个SRAM Module,在Module中通过映射后的虚拟地址直接访问该SRAM资源

SRAM Module代码如下:

/* Sram Testing Module */
……
static void sram_test(void)
{void * sram_p;char str[] = "Hello,sram!/n";sram_p = (void *)IO_ADDRESS (XXX_SRAM_BASE); /* 通过IO_ADDRESS宏得到SRAM映射后的虚拟地址 */memcpy(sram_p, str, sizeof(str));    //将 str字符数组拷贝到sram内printk(sram_p);printk("/n");
}static int __init sram_init(void)
{struct resource * ret;printk("Request SRAM mem region ............/n");ret = request_mem_region(SRAM_BASE, SRAM_SIZE, "SRAM Region");if (ret ==NULL) {printk("Request SRAM mem region failed!/n");return -1;}sram_test();return 0;
}static void __exit sram_exit(void)
{release_mem_region(SRAM_BASE, SRAM_SIZE);    printk("Release SRAM mem region success!/n");printk("SRAM is closed/n");
}module_init(sram_init);
module_exit(sram_exit);

在开发板上运行结果如下:

/ # insmod bin/sram.koRequest SRAM mem region ............Hello,sram!      ß 这句即打印的SRAM内的字符串/ # rmmod sramRelease SRAM mem region success!SRAM is close

实验发现可以通过映射后的地址正常访问SRAM。

最后,这里举SRAM作为例子的还有一个原因是通过静态映射方式访问SRAM的话,我们可以预先知道SRAM映射后的内核虚拟地址(通过IOADDRESS计算)。这样的话就可以尝试在SRAM上做点文章。比如写个内存分配的MODULE管理SRAM或者其他方式,将一些critical的数据放在SRAM内运行,这样可以提高一些复杂程序的运行效率(SRAM速度比SDRAM快多了),比如音视频的编解码过程中用到的较大的buffer等。


推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

嵌入式Linux

微信扫描二维码,关注我的公众号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465791.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

.NET 二维码生成(ThoughtWorks.QRCode)

引用ThoughtWorks.QRCode.dll &#xff08;源代码里有&#xff09; 1、简单二维码生成及解码代码&#xff1a; //生成二维码方法一private void CreateCode_Simple(string nr) { QRCodeEncoder qrCodeEncoder new QRCodeEncoder(); qrCodeEncoder.QRCodeEncodeMode QRCodeEnc…

TCP三次握手及四次挥手详解

此篇文章转载自&#xff1a;http://justim.blog.51cto.com/740099/237548TCP(Transmission Control Protocol) 传输控制协议TCP是主机对主机层的传输控制协议&#xff0c;提供可靠的连接服务&#xff0c;采用三次握手确认建立一个连接:位码即tcp标志位,有6种标示:SYN(synchron…

一文读懂 | CPU负载均衡实现

在《一文读懂 | 进程怎么绑定 CPU》这篇文章中介绍过&#xff0c;在 Linux 内核中会为每个 CPU 创建一个可运行进程队列&#xff0c;由于每个 CPU 都拥有一个可运行进程队列&#xff0c;那么就有可能会出现每个可运行进程队列之间的进程数不一样的问题&#xff0c;这就是所谓的…

NA-NP-IE系列实验28:HDLC 和PPP 封装

实验28:HDLC 和PPP 封装<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />1. 实验目的通过本实验&#xff0c;读者可以掌握如下技能&#xff1a;&#xff08;1&#xff09; 串行链路上的封装概念&#xff08;2&#xff09; HDLC 封…

使用git提交到github,每次都要输入用户名和密码的解决方法

使用git提交文件到github,每次都要输入用户名和密码&#xff0c;操作起来很麻烦&#xff0c;以下方法可解决&#xff0c;记录以下。 原因&#xff1a;在clone 项目的时候&#xff0c;使用了 https方式&#xff0c;而不是ssh方式。 默认clone 方式是&#xff1a;https 切换到&am…

回家一趟

大家好&#xff0c;我是写代码的篮球球痴。最近休年假回了一趟家里&#xff0c;决定回来也比较仓促&#xff0c;那天在公司的36楼发呆&#xff0c;觉得心里有点东西&#xff0c;然后就特别想回家看看。从晚上8&#xff1a;30出发&#xff0c;到第二天的中午&#xff0c;我从广东…

Spring MVC 使用介绍(二)—— DispatcherServlet

一、Hello World示例 1、引入依赖 <dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version><scope>provided</scope> </dependency> <dependency&g…

单片机如何检测市电通断?(应用甚广~)

我们在topemic网站上分享过一篇题为"单片机检测220V交流电通断电路"的文章&#xff0c;目前有近万次阅读&#xff0c;在这里做个总结分享给没有读过该文的公众号朋友。废话不多说&#xff0c;直接上图&#xff1a;该电路工作原理如下&#xff1a;当220V断开时&#x…

十年经验工程师为何被裁?

事件去年年底&#xff0c;公司来了一位工作十余年的工程师&#xff0c;据说软件硬件都会&#xff0c;应聘的岗位是XX算法工程师。比较巧的是&#xff0c;这位工程师是我上一家公司隔壁部门的同事。我们均来自大厂&#xff0c;但是是国企&#xff0c;二线城市。所以尽管他在前公…

飞康CEO:敢于向传统的灾备法则说“不”

近日&#xff0c;借美国飞康软件公司创办人兼首席执行官胡艾瑞徵先生访华期间&#xff0c;比特网记者对其进行了独家专访。 飞康软件公司成立于2000年&#xff0c;在过去的9年中&#xff0c; IPStor已经发展成为飞康包括整合重复数据删除功能的虚拟磁带库(VTL)、持续数据保护(C…

做10多年测试的老何

题图&#xff1a;老何是我的同事&#xff0c;他也喜欢篮球&#xff0c;因为篮球&#xff0c;我们两的话题比较多。老何做了十几年的测试工作。我们比较有猿粪的是&#xff0c;12年我在TCL&#xff0c;当时他也在TCL工业研究院&#xff0c;我们两的办公室也就相邻两栋楼。不过那…

推荐一个C++大佬

这里向大家推荐一个优质C公众号&#xff0c;号主程序喵&#xff0c;硕士毕业&#xff0c;浸淫C多年&#xff0c;帮助过不少C新手入门和进阶&#xff0c;可以说一句“精通C”啦。他搞过人脸识别&#xff0c;研究过自动驾驶&#xff0c;人生就是不断的挑战自我&#xff0c;现在从…

Silverlight HLSL实现背景滚动

一个Silverlight HLSL的简单例子&#xff0c;通过HLSL实现图片上的像素点的的水平移动&#xff0c;从而实现一个滚动背景的效果。 首先把Shader写出来吧。我这里借助了Shazzam &#xff0c;界面如下&#xff1a; 下面是我的HLSL&#xff1a; sampler2D input : register(s0);//…

极简的 PNG 编码函数 svpng(),用来学习C语言,真的很爽

这个是在知乎上看到的大神写的文章&#xff0c;如果是学习C语言入门的&#xff0c;我觉得可以从这个入手&#xff0c;特别是对图像感兴趣的。文章中提到的「我」&#xff0c;指的是「Milo Yip」大神。1. 什么是png格式图片&#xff1f;相对地&#xff0c;PNG&#xff08;Por…

STM32项目(六)—— 中文电子捡货标签

STM32项目&#xff08;六&#xff09;—— 中文电子捡货标签 宗旨&#xff1a;技术的分享是有限的&#xff0c;分享的精神是无限的。 传统物流行业仓储拣货采用纸单作业&#xff0c;拣货完成后再进行验货、出货&#xff0c;容易造成拣货错误、拣货速度与效率低、新员工培训时间…

如何把Linux工具里的“军刀”BusyBox移植到RT-Thread Smart?

RT-Thread Smart 系列连载序号内容1《当“树莓派”遇上RT-Thread Smart——应用编程入门》2《RT-Thread Smart和树莓派&#xff1a;wget & cURL网络客户端》3《如何把Linux工具里的“军刀”BusyBox移植到RT-Thread Smart&#xff1f;》4sdl图形类应用5dropbear及ssh server…

STM32项目(七) —— 智能仓库管理系统

智能仓库管理系统 随着经济的发展&#xff0c;对企业的生产经营要求提高&#xff0c;企业必须综合利用各种先进技术&#xff0c;在网络与信息技术的支持下&#xff0c;改进现在的生产经营模式和组织结构&#xff0c;增加利润。随着企业规模的扩大。高效方便的仓库管理系统&…

花三千块钱求推荐一个靠谱的C++工程师

直接说重点有个朋友想招一个C工程师&#xff0c;因为项目的原因&#xff0c;不可能现在招新人培养&#xff0c;想招到一个熟练C的工程师&#xff0c;所以想在公众号里面广而告之。如果是你推荐的人入职后&#xff0c;就可以获得三千奖励&#xff0c;如果是你本人入职&#xff0…

老外码农酒后吐槽,该说的不该说的全说了!!

上个月&#xff0c;一个有着10年码龄的程序猿喝高了&#xff0c;在社交网站Reddit上吐槽。然后被疯狂转发&#xff0c;点赞。可说是讲出了咱们很多码农的心声。咱们国内这边也有不少版本了。我们也凑凑热闹翻译一版。跟大家一块儿欣赏一下&#xff1a;今天是有点儿高了&#xf…

Intel官宣开发RISC-V处理器:明年首发7nm工艺

近日业界盛传&#xff0c;Intel计划以20亿美元收购RISC-V IP供应商SiFive——后者的产品已被80多家公司采纳&#xff0c;设计了200多种产品&#xff0c;出货量极大&#xff0c;广泛用于各种加速器。虽然双方对于收购都拒绝置评&#xff0c;但深入合作已经展开。Intel官方宣布&a…