标签:宽度 padding ble free cmd 3.1 函数指针 分区 point
MTD(Memory Technology Device)即常说的Flash等使用存储芯片的存储设备,MTD子系统对应的是块设备驱动框架中的设备驱动层,可以说,MTD就是针对Flash设备设计的标准化硬件驱动框架。本文基于3.14内核,讨论MTD驱动框架。
为了实现上述的框架, 内核中使用了如下类和API, 这些几乎是开发一个MTD驱动必须的
本身是没有list_head来供内核管理,对mtd_info对象的管理是通过mtd_part来实现的。mtd_info对象属于原始设备层,里面的很多函数接口内核已经实现了。mtd_info中的read()/write()等操作是MTD设备驱动要实现的主要函数,在NORFlash或NANDFlash中的驱动代码中几乎看不到mtd_info的成员函数,即这些函数对于Flash芯片是透明的,因为Linux在MTD的下层实现了针对NORFlash和NANDFlash的通用的mtd_info函数。
114 struct mtd_info {
115 u_char type;
116 uint32_t flags;
117 uint64_t size; // Total size of the MTD
118
123 uint32_t erasesize;
131 uint32_t writesize;
132
142 uint32_t writebufsize;
143
144 uint32_t oobsize; // Amount of OOB data per block (e.g. 16)
145 uint32_t oobavail; // Available OOB bytes per block
146
151 unsigned int erasesize_shift;
152 unsigned int writesize_shift;
153 /* Masks based on erasesize_shift and writesize_shift */
154 unsigned int erasesize_mask;
155 unsigned int writesize_mask;
156
164 unsigned int bitflip_threshold;
165
166 // Kernel-only stuff starts here.
167 const char *name;
168 int index;
169
170 /* ECC layout structure pointer - read only! */
171 struct nand_ecclayout *ecclayout;
172
173 /* the ecc step size. */
174 unsigned int ecc_step_size;
175
176 /* max number of correctible bit errors per ecc step */
177 unsigned int ecc_strength;
178
179 /* Data for variable erase regions. If numeraseregions is zero,
180 * it means that the whole device has erasesize as given above.
181 */
182 int numeraseregions;
183 struct mtd_erase_region_info *eraseregions;
184
185 /*
186 * Do not call via these pointers, use corresponding mtd_*()
187 * wrappers instead.
188 */
189 int (*_erase) (struct mtd_info *mtd, struct erase_info *instr);
190 int (*_point) (struct mtd_info *mtd, loff_t from, size_t len,
191 size_t *retlen, void **virt, resource_size_t *phys);
192 int (*_unpoint) (struct mtd_info *mtd, loff_t from, size_t len);
193 unsigned long (*_get_unmapped_area) (struct mtd_info *mtd,
194 unsigned long len,
195 unsigned long offset,
196 unsigned long flags);
197 int (*_read) (struct mtd_info *mtd, loff_t from, size_t len,
198 size_t *retlen, u_char *buf);
199 int (*_write) (struct mtd_info *mtd, loff_t to, size_t len,
200 size_t *retlen, const u_char *buf);
201 int (*_panic_write) (struct mtd_info *mtd, loff_t to, size_t len,
202 size_t *retlen, const u_char *buf);
203 int (*_read_oob) (struct mtd_info *mtd, loff_t from,
204 struct mtd_oob_ops *ops);
205 int (*_write_oob) (struct mtd_info *mtd, loff_t to,
206 struct mtd_oob_ops *ops);
207 int (*_get_fact_prot_info) (struct mtd_info *mtd, struct otp_info *buf,
208 size_t len);
209 int (*_read_fact_prot_reg) (struct mtd_info *mtd, loff_t from,
210 size_t len, size_t *retlen, u_char *buf);
211 int (*_get_user_prot_info) (struct mtd_info *mtd, struct otp_info *buf,
212 size_t len);
213 int (*_read_user_prot_reg) (struct mtd_info *mtd, loff_t from,
214 size_t len, size_t *retlen, u_char *buf);
215 int (*_write_user_prot_reg) (struct mtd_info *mtd, loff_t to,
216 size_t len, size_t *retlen, u_char *buf);
217 int (*_lock_user_prot_reg) (struct mtd_info *mtd, loff_t from,
218 size_t len);
219 int (*_writev) (struct mtd_info *mtd, const struct kvec *vecs,
220 unsigned long count, loff_t to, size_t *retlen);
221 void (*_sync) (struct mtd_info *mtd);
222 int (*_lock) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
223 int (*_unlock) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
224 int (*_is_locked) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
225 int (*_block_isbad) (struct mtd_info *mtd, loff_t ofs);
226 int (*_block_markbad) (struct mtd_info *mtd, loff_t ofs);
227 int (*_suspend) (struct mtd_info *mtd);
228 void (*_resume) (struct mtd_info *mtd);
229 /*
230 * If the driver is something smart, like UBI, it may need to maintain
231 * its own reference counting. The below functions are only for driver.
232 */
233 int (*_get_device) (struct mtd_info *mtd);
234 void (*_put_device) (struct mtd_info *mtd);
235
236 /* Backing device capabilities for this device
237 * - provides mmap capabilities
238 */
239 struct backing_dev_info *backing_dev_info;
240
241 struct notifier_block reboot_notifier; /* default mode before reboot */
242
243 /* ECC status information */
244 struct mtd_ecc_stats ecc_stats;
245 /* Subpage shift (NAND) */
246 int subpage_sft;
247
248 void *priv;
249
250 struct module *owner;
251 struct device dev;
252 int usecount;
253 };
struct mtd_info
–115–>MTD设备类型,有MTD_RAM,MTD_ROM、MTD_NORFLASH、MTD_NAND_FLASH
–116–>读写及权限标志位,有MTD_WRITEABLE、MTD_BIT_WRITEABLE、MTD_NO_ERASE、MTD_UP_LOCK
–117–>MTD设备的大小
–123–>主要的擦除块大小,NandFlash就是”块”的大小
–131–>最小可写字节数,NandFlash一般对应”页”的大小
–144–>一个block中的OOB字节数
–145–>一个block中可用oob的字节数
–171–>ECC布局结构体指针
–190–>针对eXecute-In-Place,即XIP
–192–>如果这个指针为空,不允许XIP
–197–>读函数指针
–199–>写函数指针
–248–>私有数据
内核管理分区的链表节点,通过它来实现对mtd_info对象的管理。
41 struct mtd_part {
42 struct mtd_info mtd;
43 struct mtd_info *master;
44 uint64_t offset;
45 struct list_head list;
46 };
struct mtd_part
–42–>对应的mtd_info对象
–43–>父对象指针
–44–>偏移量
–45–>链表节点
描述一个分区
39 struct mtd_partition {
40 const char *name; /* identifier string */
41 uint64_t size; /* partition size */
42 uint64_t offset; /* offset within the master MTD space */
43 uint32_t mask_flags; /* master MTD flags to mask out for this partition */
44 struct nand_ecclayout *ecclayout; /* out of band layout for this partition (NAND only) */
45 };
mtd_partition
–40–>分区名
–41–>分区大小,使用MTDPART_SIZ_FULL表示使用全部空间
–42–>分区在master设备中的偏移量。MTDPART_OFS_APPEND表示从上一个分区结束的地方开始,MTDPART_OFS_NXTBLK表示从下一个擦除块开始; MTDPART_OFS_RETAIN表示尽可能向后偏,把size大小的空间留下即可
–43–>权限掩码,MTD_WRITEABLE表示将父设备的只读选项变成可写(可写分区要求size和offset要erasesize对齐,eg MTDPART_OFS_NEXTBLK)
–44–>NANDFlash的OOB布局,OOB是NANDFlash中很有用空间,比如yaffs2就需要将坏块信息存储在OOB区域
链表头,将所有的mtd_partition连接起来。
36 /* Our partition linked list */
37 static LIST_HEAD(mtd_partitions);
下图是关键API的调用关系。
mtd_add_partition()
└── add_mtd_device()
add_mtd_partitions()
└── add_mtd_device()
分配并初始化一个mtd对象。
367 334 int add_mtd_device(struct mtd_info *mtd)
335 {
336 struct mtd_notifier *not;
337 int i, error;
338
339 if (!mtd->backing_dev_info) {
340 switch (mtd->type) {
341 case MTD_RAM:
342 mtd->backing_dev_info = &mtd_bdi_rw_mappable;
343 break;
344 case MTD_ROM:
345 mtd->backing_dev_info = &mtd_bdi_ro_mappable;
346 break;
347 default:
348 mtd->backing_dev_info = &mtd_bdi_unmappable;
349 break;
350 }
351 }
355
356 i = idr_alloc(&mtd_idr, mtd, 0, 0, GFP_KERNEL);
357 if (i < 0)
358 goto fail_locked;
359
360 mtd->index = i;
361 mtd->usecount = 0;
362
363 /* default value if not set by driver */
364 if (mtd->bitflip_threshold == 0)
365 mtd->bitflip_threshold = mtd->ecc_strength;
366
367 if (is_power_of_2(mtd->erasesize))
368 mtd->erasesize_shift = ffs(mtd->erasesize) - 1;
369 else
370 mtd->erasesize_shift = 0;
371
372 if (is_power_of_2(mtd->writesize))
373 mtd->writesize_shift = ffs(mtd->writesize) - 1;
374 else
375 mtd->writesize_shift = 0;
376
377 mtd->erasesize_mask = (1 << mtd->erasesize_shift) - 1;
378 mtd->writesize_mask = (1 << mtd->writesize_shift) - 1;
379
380 /* Some chips always power up locked. Unlock them now */
381 if ((mtd->flags & MTD_WRITEABLE) && (mtd->flags & MTD_POWERUP_LOCK)) {
382 error = mtd_unlock(mtd, 0, mtd->size);
387 }
388
392 mtd->dev.type = &mtd_devtype;
393 mtd->dev.class = &mtd_class;
394 mtd->dev.devt = MTD_DEVT(i);
395 dev_set_name(&mtd->dev, "mtd%d", i);
396 dev_set_drvdata(&mtd->dev, mtd);
397 if (device_register(&mtd->dev) != 0)
399
400 if (MTD_DEVT(i))
401 device_create(&mtd_class, mtd->dev.parent,
402 MTD_DEVT(i) + 1,
403 NULL, "mtd%dro", i);
408 list_for_each_entry(not, &mtd_notifiers, list)
409 not->add(mtd);
417 return 0;
424 }
add_mtd_device()
–395–>设置MTD设备的名字
–396–>设置私有数据,将mtd地址藏到device->device_private->void* driver_data
–408–>遍历所有的mtd_notifier,将其添加到通知链
通过将一个mtd_part对象注册到内核,将mtd_info对象注册到内核,即为一个设备添加一个分区。
537 int mtd_add_partition(struct mtd_info *master, const char *name,
538 long long offset, long long length)
539 {
540 struct mtd_partition part;
541 struct mtd_part *p, *new;
542 uint64_t start, end;
543 int ret = 0;
545 /* the direct offset is expected */
546 if (offset == MTDPART_OFS_APPEND ||
547 offset == MTDPART_OFS_NXTBLK)
548 return -EINVAL;
549
550 if (length == MTDPART_SIZ_FULL)
551 length = master->size - offset;
552
553 if (length <= 0)
554 return -EINVAL;
555
556 part.name = name;
557 part.size = length;
558 part.offset = offset;
559 part.mask_flags = 0;
560 part.ecclayout = NULL;
561
562 new = allocate_partition(master, &part, -1, offset);
563 if (IS_ERR(new))
564 return PTR_ERR(new);
565
566 start = offset;
567 end = offset + length;
568
569 mutex_lock(&mtd_partitions_mutex);
570 list_for_each_entry(p, &mtd_partitions, list)
571 if (p->master == master) {
572 if ((start >= p->offset) &&
573 (start < (p->offset + p->mtd.size)))
574 goto err_inv;
575
576 if ((end >= p->offset) &&
577 (end < (p->offset + p->mtd.size)))
578 goto err_inv;
579 }
580
581 list_add(&new->list, &mtd_partitions);
582 mutex_unlock(&mtd_partitions_mutex);
583
584 add_mtd_device(&new->mtd);
585
586 return ret;
591 }
626 int add_mtd_partitions(struct mtd_info *master,
627 const struct mtd_partition *parts,
628 int nbparts)
629 {
630 struct mtd_part *slave;
631 uint64_t cur_offset = 0;
632 int i;
636 for (i = 0; i < nbparts; i++) {
637 slave = allocate_partition(master, parts + i, i, cur_offset);
642 list_add(&slave->list, &mtd_partitions);
645 add_mtd_device(&slave->mtd);
647 cur_offset = slave->offset + slave->mtd.size;
648 }
649
650 return 0;
651 }
MTD设备提供了字符设备和块设备两种接口,对于字符设备接口,在“drivers/mtd/mtdchar.c”中实现了,比如,用户程序可以直接通过ioctl()回调相应的驱动实现。其中下面的几个是这些操作中常用的结构,这些结构是对用户空间开放的,类似于输入子系统中的input_event结构。
//include/uapi/mtd/mtd-abi.h
125 struct mtd_info_user {
126 __u8 type;
127 __u32 flags;
128 __u32 size; /* Total size of the MTD */
129 __u32 erasesize;
130 __u32 writesize;
131 __u32 oobsize; /* Amount of OOB data per block (e.g. 16) */
132 __u64 padding; /* Old obsolete field; do not use */
133 };
描述NandFlash的OOB(Out Of Band)信息。
35 struct mtd_oob_buf {
36 __u32 start;
37 __u32 length;
38 unsigned char __user *ptr;
39 };
25 struct erase_info_user {
26 __u32 start;
27 __u32 length;
28 };
mtd_oob_buf oob;
erase_info_user erase;
mtd_info_user meminfo;
/* 获得设备信息 */
if(0 != ioctl(fd, MEMGETINFO, &meminfo))
perror("MEMGETINFO");
/* 擦除块 */
if(0 != ioctl(fd, MEMERASE, &erase))
perror("MEMERASE");
/* 读OOB */
if(0 != ioctl(fd, MEMREADOOB, &oob))
perror("MEMREADOOB");
/* 写OOB??? */
if(0 != ioctl(fd, MEMWRITEOOB, &oob))
perror("MEMWRITEOOB");
/* 检查坏块 */
if(blockstart != (ofs & (~meminfo.erase + 1))){
blockstart = ofs & (~meminfo.erasesize + 1);
if((badblock = ioctl(fd, MEMGETBADBLOCK, &blockstart)) < 0)
perror("MEMGETBADBLOCK");
else if(badblock)
/* 坏块代码 */
else
/* 好块代码 */
}
NANDFlash和NORFlash都是基于MTD框架编写的,由于MTD框架中通用代码已经在内核中实现了,所以驱动开发主要是进行MTD框架中的的开发。
基于上述的MTD框架, Flash驱动都变的十分的简单, 因为当下Flash的操作接口已经很统一, a, 相应的代码在“drivers/mtd/chips”中文件实现,所以在设备驱动层, 留给驱动工程师的工作就大大的减少了。
基于MTD子系统开发NOR FLash驱动,只需要构造一个map_info类型的对象并调用do_map_probe()来匹配内核中已经写好的驱动,比如CFI接口的驱动或JEDEC接口的驱动。当下编写一个NorFlash驱动的工作流程如下
208 struct map_info {
209 const char *name;
210 unsigned long size;
211 resource_size_t phys;
212 #define NO_XIP (-1UL)
214 void __iomem *virt;
215 void *cached;
217 int swap; /* this mapping‘s byte-swapping requirement */
218 int bankwidth;
243 void (*set_vpp)(struct map_info *, int);
245 unsigned long pfow_base;
246 unsigned long map_priv_1;
247 unsigned long map_priv_2;
248 struct device_node *device_node;
249 void *fldrv_priv;
250 struct mtd_chip_driver *fldrv;
251 };
struct map_info
–210–>NOR Flash设备的容量
–211–>NOR Flash在物理地址空间中的地址
–214–>由物理地址映射的虚拟地址
–218–>总线宽度,NOR Flash是有地址总线的,所以才能片上执行,一般都是8位或16位宽
构造好一个map_info对象之后,接下来的工作就是匹配驱动+注册分区表
这个API用来根据传入的参数匹配一个map_info对象的驱动,比如CFI接口或JEDEC接口的NOR Flash。这个函数的接口如下:
struct mtd_info *do_map_probe(const char *name, struct map_info *map)
对于常用的NorFlash标准,这个函数的调用方式如下:
do_map_probe("cfi_probe", &xxx_map_info);
do_map_probe("jedec_probe",&xxx_map_info);
do_map_probe("map_rom",&xxx_map_info);
匹配了设备驱动,可以发现一个map_info对象中没有mtd_partitions相关的信息,对于一个NOR Flash的分区信息,需要通过do_map_probe返回的mtd_info对象来注册到内核。这里我们可以先调用parse_mtd_partitions()查看Flash上已有的分区信息,获取了分区信息之后再调用add_mtd_partitions()将分区信息写入内核
#define WINDOW_SIZE ...
#define WINDOW_ADDR ...
static struct map_info xxx_map = {
.name = "xxx flash",
.size = WINDOW_SIZE,
.bankwidth = 1,
.phys = WINDOW_ADDR,
};
static struct mtd_partition xxx_partitions[] = {
.name = "Drive A",
.offset = 0,
.size = 0x0e000,
};
#define NUM_PARTITIONS ARRAY_SIZE(xxx_partitions)
static struct mtd_info *mymtd;
static int __init init_xxx_map(void)
{
int rc = 0;
xxx_map.virt = ioremap_nocache(xxx_map.phys, xxx_map.size);
if(!xxx_map.virt){
printk(KERN_ERR"Failed to ioremap_nocache\n");
rc = -EIO;
goto err2;
}
simple_map_init(&xxx_map);
mymtd = do_map_probe("jedec_probe", &xxx_map);
if(!mymtd){
rc = -ENXIO;
goto err1;
}
mymtd->owner = THIS_MODULE;
add_mtd_partitions(mymtd, xxx_partitions, NUM_PARTITIONS);
return 0;
err1:
map_destroy(mymtd);
iounmap(xxx_map.virt);
err2:
return rc;
}
static void __exit cleanup_xxx_map(void)
{
if(mymtd){
del_mtd_partitions(mymtd);
map_destroy(mymtd);
}
if(xxx_map.virt){
iounmap(xxx_map.virt);
xxx_map.virt = NULL;
}
}
下图就是基于MTD框架的NandFlash驱动的位置。
Nand Flash和NOR Flash类似,内核中已经在“drivers/mtd/nand/nand_base.c”中实现了通用的驱动程序,驱动开发中不需要再实现mtd_info中的read, write, read_oob, write_oob等接口,只需要构造并注册一个nand_chip对象, 这个对象主要描述了一片flash芯片的相关信息,包括地址信息,读写方法,ECC模式,硬件控制等一系列底层机制。当下,编写一个NandFlash驱动的工作流程如下:
这个结构描述一个NAND Flash设备,通常藏在mtd_info->priv中,以便在回调其中的接口的时候可以找到nand_chip对象。
547 struct nand_chip {
548 void __iomem *IO_ADDR_R;
549 void __iomem *IO_ADDR_W;
550
551 uint8_t (*read_byte)(struct mtd_info *mtd);
578
579 int chip_delay;
580 unsigned int options;
581 unsigned int bbt_options;
582
583 int page_shift;
584 int phys_erase_shift;
585 int bbt_erase_shift;
586 int chip_shift;
587 int numchips;
588 uint64_t chipsize;
589 int pagemask;
590 int pagebuf;
591 unsigned int pagebuf_bitflips;
592 int subpagesize;
593 uint8_t bits_per_cell;
594 uint16_t ecc_strength_ds;
595 uint16_t ecc_step_ds;
596 int badblockpos;
597 int badblockbits;
598
599 int onfi_version;
600 struct nand_onfi_params onfi_params;
601
602 int read_retries;
603
604 flstate_t state;
605
606 uint8_t *oob_poi;
607 struct nand_hw_control *controller;
608
609 struct nand_ecc_ctrl ecc;
610 struct nand_buffers *buffers;
611 struct nand_hw_control hwcontrol;
612
613 uint8_t *bbt;
614 struct nand_bbt_descr *bbt_td;
615 struct nand_bbt_descr *bbt_md;
616
617 struct nand_bbt_descr *badblock_pattern;
618
619 void *priv;
620 };
struct nand_chip
–609–>NAND芯片的OOB分布和模式,如果不赋值,则会使用内核默认的OOB
–580–>与具体的NAND 芯片相关的一些选项,如NAND_BUSWIDTH_16 等,可以参考<Linux/mtd/nand.h>
–583–>用位表示的NAND 芯片的page 大小,如某片NAND 芯片的一个page 有512 个字节,那么page_shift 就是9 ;
–584–>用位表示的NAND 芯片的每次可擦除的大小,如某片NAND 芯片每次可擦除16K 字节( 通常就是一个block 的大小) ,那么phys_erase_shift 就是14 ;
–585–>用位表示的bad block table 的大小,通常一个bbt 占用一个block ,所以bbt_erase_shift 通常与phys_erase_shift 相等;
–587–>表示系统中有多少片NAND 芯片;
–588–>NAND 芯片的大小;
–589–>计算page number 时的掩码,总是等于chipsize/page 大小 - 1 ;
–590–>用来保存当前读取的NAND 芯片的page number ,这样一来,下次读取的数据若还是属于同一个page ,就不必再从NAND 芯片读取了,而是从data_buf 中直接得到;
–596–>表示坏块信息保存在oob 中的第几个字节。对于绝大多数的NAND 芯片,若page size> 512,那么坏块信息从Byte 0 开始存储,否则就存储在Byte 5 ,即第六个字节。
–619–>私有数据
准备好了一个nand_chip,接下来的工作就是匹配驱动+注册分区表。
NAND flash使用nand_scan()来匹配驱动,这个函数会读取NAND芯片的ID,并根据mtd->priv即nand_chip中的成员初始化mtd_info。如果要分区,则以mtd_info和mtd_partition为参数调用add_mtd_partitions来添加分区信息。
int nand_scan(struct mtd_info *mtd, int maxchips)
#define CHIP_PHYSICAL_ADDRESS ...
#define NUM_PARTITIONS 2
static struct mtd_partition partition_info[] = {
{
.name = "Flash partition 1",
.info = 0,
.size = 8 * 1024 * 1024,
},
{
.name = "Flash partition 2",
offset = MTDPART_OFS_NEXT,
size = MTDPART_SIZ_FULL,
},
};
int __init board_init(void)
{
struct nand_chip *this;
int err = 0;
/* 为MTD设备对象和nand_chip分配内存 */
board_mtd = kmalloc(sizeof(struct mtd_info) + sizeof(struct nand_chip),GFP_KERNEL);
if(!board_mtd){
printk("Unable to allocate NAND MTD device structure\n");
err = -ENOMEM;
goto out;
}
/* 初始化结构体 */
memset((char *)board_mtd, 0 ,sizeof(struct mtd_info) + sizeof(struct nand_chip));
/* 映射物理地址 */
baseaddr = (unsigned long) ioremap(CHIP_PHYSICAL_ADDRESS,1024);
if(!baseaddr){
printk("Ioremap to access NAND Chip failed\n");
err = -EIO;
goto out_mtd;
}
/* 获取私有数据(nand_chip)指针 */
this = (struct nand_chip *)(&board_mtd[1]);
/* 将nand_chip赋予mtd_info私有指针 */
board_mtd->priv = this;
/* 设置NAND Flash的IO基地址 */
this->IO_ADDR_R = baseaddr;
this->IO_ADDR_W = baseaddr;
/* 硬件控制函数 */
this->cmd_ctrl = board_hwcontrol;
/* 初始化设备ready函数 */
this->dev_ready = board_dev_ready;
/* 扫描以确定设备的存在 */
if(nand_scan(board_mtd, 1)){
err = -ENXIO;
goto = out_ior;
}
/* 添加分区 */
add_mtd_partitions(board_mtd,partition_info,NUM_PARTITIONS);
goto out;
out_ior:
iounmap((void *)baseaddr);
out_mtd:
kfree(board_mtd);
out:
return err;
}
static void __exit board_cleanup(void)
{
/* 释放资源,注销设备 */
nand_release(board_mtd);
/* unmap物理地址 */
iounmap((void *)baseaddr);
/* 释放MTD设备结构体 */
kfree(board_mtd);
}
/* 硬件控制 */
static void board_hwcontrol(struct mtd_info *mtd, int dat,unsigned int ctrl)
{
...
if(ctrl & NAND_CTRL_CHANGE){
if(ctrl & NAND_NCE){
}
}
...
}
/*返回ready状态*/
static int board_dev_ready(struct mtd_info *mtd)
{
return xxx_read_ready_bit();
}
标签:宽度 padding ble free cmd 3.1 函数指针 分区 point
原文地址:http://www.cnblogs.com/cppdaxue/p/6626757.html