ARM微控制器-MCU基础及CPU运行过程

目录

 一. CPU的基本结构和运行机制

1. 一个基本的MCU内部结构

2. MCU Structure

3. 分析其中的CPU:

一个完整的CPU:

4. 堆栈

5. 堆栈溢出(难点)

6. 一种常见的CPU模型(16bit):

7. 一个简单的CPU运行的例子:

8. ARM架构CPU的体系结构

寄存器组:

CPU的工作状态:

9. 例子:ARM Cortex M0 CPU程序运行过程

10. 中断:

中断服务子程:

中断优先级:

中断嵌套:

中断函数使用全局变量的潜在风险:

11. 复位、时钟、存储器和总线

 (1)复位

(2)时钟

(3)存储器

(4)总线

一个例子:从CPU视角按地址段来看待外围世界。

二. MCU基础总结

在地址框图的模型里,程序究竟是怎么运行的呢?


参考《ARM微控制器与嵌入式系统--清华》

NXP MPC5748G:32位Power Architecture MCU,

NXP S32K148:32位ARM Cortex-M4F内核MCU,

Infineon TC387:32位TriCore微控制器,32-bit Arm Cortex Microcontroller(MCU) Families by Cypress(赛普拉斯半导体已并入英飞凌)。

据说NXP之后会逐渐放弃Power架构的MCU,转战Arm架构?

 一. CPU的基本结构和运行机制

1. 一个基本的MCU内部结构

ARM微控制器-MCU基础及CPU运行过程

2. MCU Structure

ARM微控制器-MCU基础及CPU运行过程

3. 分析其中的CPU:

(ALU、寄存器组、控制单元是必要的,其他非必要)

ARM微控制器-MCU基础及CPU运行过程

一个完整的CPU:

ARM微控制器-MCU基础及CPU运行过程

将ALU拿出来:

ARM微控制器-MCU基础及CPU运行过程

分析其中的Quiz:

① A + B = C

操作数:A B;运算:+;运算结果:C;

标志位:比如说用10+9=19,19无法用4个bit的二进制数表示,所以CPU需要以某种方式将这些记录下来,即溢出的标志位。

② A > B ?

操作数:A B;运算:>(本质是减法运算,最后要的不是结果,而是status,如果A-B得到了一个最高位为1的补码,即负数,说明A<B);

标志位:记录产生的结果是0 or 1。

③ A>>=1(A=A>>1)

操作数:A;运算:>>=;运算结果:一个数;标志位:当右移至0时产生一个标志。

ALU产生的结果及状态位需要保存下来,大部分CPU会由register来完成,CPU中的这种register叫做程序状态寄存器(PSR,Program Status Register)或CCR。

ARM微控制器-MCU基础及CPU运行过程

那么操作数又是如何加载到CPU的呢?

ARM微控制器-MCU基础及CPU运行过程

操作数不仅可以从有限的寄存器中获取,还可以通过外部的存储器获得。

ARM微控制器-MCU基础及CPU运行过程

然后,控制单元负责将一条确定的指令解析为对存储器、寄存器、ALU的控制,来明确我们要做什么操作。

 

ARM微控制器-MCU基础及CPU运行过程

但是,控制单元所需的指令从何而来呢?

        所谓程序,就是指令的集合,而每一个指令又是0和1组成的机器码放在存储器中。在时序电路的驱动下,依次把存储器中的指令读取出来放在控制单元中。在这个过程中,会有一个PC寄存器(也叫程序计数器)参与进来,也叫PC指针寄存器,因为这个寄存器一直保存下一条将要执行的指令的地址。总而言之,它是一个指针。

ARM微控制器-MCU基础及CPU运行过程

       C语言翻译成汇编再翻译成指令,逐条执行下去。但是当指令序列出现分支时,PC将不再是按序递增到相邻的下一条指令。如下,offset--偏移量。

ARM微控制器-MCU基础及CPU运行过程

        但是,问题:如果调用一个函数,它跳转到一个新的地址去执行,函数执行完后要返回,那么CPU如何知道自己该返回哪里呢?

ARM微控制器-MCU基础及CPU运行过程

答:早起CPU会增加一个返回地址寄存器,但是如果函数出现嵌套调用,就会要求有多个返回地址寄存器。寄存器不可能无限制地加下去,所以出现了一个机制----堆栈。

4. 堆栈

1. 基本概念:
ARM微控制器-MCU基础及CPU运行过程

2. 作用:

ARM微控制器-MCU基础及CPU运行过程

所以说,为什么在C语言中,局部变量是有生命周期的?因为当一个函数返回的时候,其存储空间就被释放,这个变量就不再能被使用。

3. 运行机制:(一定要记住:一旦当进去一定要拿走,最后放进去的最先取出来)

ARM微控制器-MCU基础及CPU运行过程

完整的CPU构架:

ALU只负责计算,它的数可以从寄存器来,也可以从片外存储器来;

数从哪儿来,做什么运算,是由指令解析产生的,逻辑控制单元来控制;

逻辑控制单元的指令是在PC指针寄存器的指引下,从程序的地址空间拿进来逐条解析;

在这些过程中如果PC寄存器不是逐一累加的,就需要在SP的指引下,把当前PC指针寄存器想要返回的值存在一个特定的内存空间(即栈中),来应付程序之间的函数调用。

      堆是从系统内存的顶部开始使用的一段存储空间,一般这个空间是全局的。系统中所有动态分配的对象(比如指针)都是在这个空间上分配。但在没有OS的嵌入式系统中,通常不使用堆,可以认为堆相对小。

5. 堆栈溢出(难点)

堆在上面使用内存,栈在下面动态涨落。

这样下去,总会有一种风险,栈的使用逐渐接近堆,在某一时刻堆和栈碰上了,就会产生堆栈溢出。

      有两个地方会比较容易遇到堆栈溢出。比如windows的蓝屏,以及越界访问。一旦溢出,堆栈在pull出来的时候,它认为应该把这一段存储器的值作为函数的返回值,但是由于已经溢出,这段存储器的值可能被别的全局变量使用了,于是这个值被错误地取出来当做函数的返回值,PC指针寄存器指向了存储器中一个位置的空间,并试图把其中的值拿出来进行执行。这时候就说“程序跑飞了”。对于操作系统,它可能认为这是越界访问,就蓝屏了;对于嵌入式系统,会认为这是非法指令,无法解释,于是程序就复位了。

了解一种CPU时:

了解其可编程的寄存器组;

了解其指令集。

6. 一种常见的CPU模型(16bit):

ARM微控制器-MCU基础及CPU运行过程

一个16bit数据寄存器,称作D,根据需要,可以把前8bit和后8bit拆开当做两个8bit数据寄存器;两个16bit寻址寄存器X和Y,可以保存外部存储地址、指针;一个堆栈指针寄存器SP,一个PC指针寄存器和一个完整的标志位。

7. 一个简单的CPU运行的例子:

(1)初始状态:蓝色箭头表示当前状态(即已经执行完的指令),PC=0x3006表示下一条将要执行的指令所处的地址,A和B两个数据寄存器各有一个初值。

ARM微控制器-MCU基础及CPU运行过程

(2)给SP加载一个值0x2000,PC指向下一条待执行的指令的地址,内存空间那里出现了一个红色剪头,指向0x2000地址,所以它表示堆栈指针寄存器所指向的地址。这条指令完成了堆栈的初始化,指定了红色箭头往上这段闲置的内存空间个堆栈使用。

ARM微控制器-MCU基础及CPU运行过程

(3)把CPU内部的A寄存器的值push给栈(a值入栈)。

ARM微控制器-MCU基础及CPU运行过程

(4)b值入栈。

ARM微控制器-MCU基础及CPU运行过程

(5)调用子函数。蓝色箭头执行完后,PC指向子函数的第一条指令,堆栈中多出2bit的内容(0x30和0x0C),这两个字节合起来是一个16bit的完整地址0x300C,对应调用函数指令的下一条指令(即函数返回后,继续执行下一条指令,这是一个自动的压栈过程),SP指向0x1FFC。

ARM微控制器-MCU基础及CPU运行过程

(6)子函数第一条指令NOP,什么操作也没有,只是PC的值变为下一条指令的地址。

ARM微控制器-MCU基础及CPU运行过程

(7)RTS表示从子函数返回,PC变成0x300C,从哪里来的呢?堆栈指针寄存器往下移了两位(释放了两个单元),把0x30和0x0C取出来赋给了PC,即子函数执行完后返回的地址。(注意:30和0C两个值还在内存里,只是不再被堆栈所使用了,下次再往这个地址放值的时候回直接覆盖掉之前的。)

ARM微控制器-MCU基础及CPU运行过程

(8)从堆栈中pull出一个地址为0x1FFE的数赋给A寄存器。堆栈指针寄存器的值加1,堆栈又释放一个bit。

ARM微控制器-MCU基础及CPU运行过程

(9)从堆栈中pull出一个地址为0x1FFF的数赋给B寄存器。堆栈回到了初始状态,整个程序跑完,PC指针寄存器指向了下一个地址。

ARM微控制器-MCU基础及CPU运行过程

     通过这个例子,熟悉了16bit的MCU,汇编指令,寄存器模型程序的加载和内存堆栈的使用。

      上述这个程序实现了A和B两个值的交换。

8. ARM架构CPU的体系结构

寄存器组:

以ARM Cortex M的32位CPU为例。

ARM微控制器-MCU基础及CPU运行过程

      从寄存器上看,在数据和地址的通用寄存器上,ARM Cortex M的内核基本都是上图所示的寄存器组:包括R0 ~ R15共计16个32bit的寄存器。其中,R0~R12为通用寄存器,R13规定为堆栈指针寄存器,它总是保存一个地址(可以理解为一个指针),指向堆栈所使用的内存地址;R15是PC指针寄存器,它也保存一个地址,就是程序运行到第几条指令的那个指令的地址;R14(Link register),它是在发生函数调用、程序跳转的时候,自动保存一个函数的返回地址。(最早的CPU,返回地址是保存在寄存器内,后来函数的调用、嵌套越来越多,才开始使用堆栈。使用Link Register的话,可以使程序在发生单次调用的时候,使它的跳转和调用变快很多。)

      在ARM的体系结构里,寄存器被总体分为了5类:

①xPSR-Program status registers,保存CPU的各种设置状态,便于读写访问;

②PRIMASK:主要是设置中断的开关,一个独立的寄存器;

③FAULTMASK:控制不可屏蔽中断的开、关;

④BASEPRI:控制按优先级关闭中断的功能;

⑤CONTROL:只有1-2bit可以用,设置CPU实际的工作模式和状态。

在Cortex-M3/4中有③和④,Cortex-M0中没有③和④。

      xPSR是一个32bit的状态寄存器,每一个bit都有自己唯一的功能,便于我们对它进行访问,获得CPU的状态。它有三个别名,用别名访问寄存器的时候,只关注和读取其中特定的字段,来实现单一的功能。(比如用APSR这个名字访问这个寄存器,读到的是高4位的值,后面都是Reserved,高4位的值存的就是是否有得0、是否有负数、是否有借位进位、是否有溢出这样一些基本的指令运算产生的状态位。用IPSR时,高几位都是Reserved,Expection number保留了中断时候发生异常的中断号。EPSR的T主要用来记录是否发生异常、是否发生中断。)

ARM微控制器-MCU基础及CPU运行过程

PRIMASK ,优先级mask寄存器。这里只使用最低的1位,这1位不是根据优先级控制中断,而是控制了所有中断的使能和关闭。所以它是控制中断的总开关

ARM微控制器-MCU基础及CPU运行过程

CONTROL Register:CPU的状态寄存器,有2个bit可以使用。bit0控制CPU工作在用户态还是特权态(在M0和M0+的CPU上,这两种状态是不加以区分的,所以是Reserved;M3和M4是区分的),bit1规定了所使用的堆栈是主堆栈还是进程堆栈。

返回去看R13,堆栈指针寄存器有两个,分别指向内存的两个不同的区域,一个堆栈给操作系统的内核使用,一个堆栈给用户程序(任务)使用。这样当任务跑飞的时候,不会使操作系统崩溃。而bit1就是用来设置当前程序使用主堆栈还是副堆栈。

ARM微控制器-MCU基础及CPU运行过程

CPU的工作状态:

对于ARM Cortex M0/M0+来讲,工作状态只有两个:

thread mode:进程模式,即跑程序时候,简单理解为运行main和main的调用;

handle mode:句柄模式,发生中断时响应中断的状态。

M3和M4,为了考虑实时操作系统的使用、考虑到实时操作系统的内核和用户程序的区分,出现了用户态和特权态的区分。

ARM微控制器-MCU基础及CPU运行过程

M0、M1的MCU是M3、M4的MCU的子集,并且高度兼容。

9. 例子:ARM Cortex M0 CPU程序运行过程

蓝色的箭头总是指向上一条执行完的指令,红色的箭头总是指向堆栈中已经使用过的内存地址。

如下图,当前状态下,表示刚刚执行完的指令(是在02这个地址上的nop指令)什么也没有做。这条指令执行完后,PC指针寄存器变为0x0000 0804。

ARM微控制器-MCU基础及CPU运行过程

执行下一步,movs r4,#18表示把十进制数18传递给r4。所以,r4的值变为0x0000 0806,PC指针寄存器变为下一条指令的地址。

ARM微控制器-MCU基础及CPU运行过程

下一步,

ARM微控制器-MCU基础及CPU运行过程

下一步,把r4的值压栈,

ARM微控制器-MCU基础及CPU运行过程

下一步,

ARM微控制器-MCU基础及CPU运行过程

下一步,在这个一级调用里,堆栈暂时没被使用,返回地址到了LR(Link Register)里边。问题来了:LR=0x0000 0811,是奇数,但是程序的地址都是偶数。为什么?

       因为ARM Cortex M的指令都是16bit、32bit宽的,所以指令占的空间都是2个2个4个4个地在用,所以地址都是偶数。这是ARM体系结构的向下兼容的特性。当保存的返回地址的最低位是1的时候,函数调用或任何跳转返回的时候,CPU仍然工作在thumb指令集的状态下。而如果返回地址的最低位为0,就表示告诉CPU在返回的时候应该切换至ARM指令集的状态下。因为现在使用的Cortex M体系结构的CPU,所有的跳转返回地址被自动置为1,而在返回的时候,这一位被忽略,只取它的偶数部分作为返回地址。所以,存的是0811,实际是返回0810这个地址。

ARM微控制器-MCU基础及CPU运行过程

下一步,因为子函数第一步啥也没干,所以只有PC指向下一条指令。

ARM微控制器-MCU基础及CPU运行过程

下一步,根据LR寄存器的值,进行跳转,回到主函数中。

ARM微控制器-MCU基础及CPU运行过程

下一步,弹出一个值,返回给r4寄存器,覆盖了r4原先的值。

ARM微控制器-MCU基础及CPU运行过程

下一步,堆栈回到初始状态,PC指向下一条指令的地址。

ARM微控制器-MCU基础及CPU运行过程

这个程序的功能:交换了r4和r5的值。

10. 中断:

中断服务子程:

        在ARM Cortex M平台上,中断服务子程与一般C函数写法没有区别,使用同样的汇编返回指令即可。(原因:ARM Cortex M处理器在发生中断的时候,会在LR寄存器R14里保存一个特殊值,从而在使用一个普通的函数返回指令时,CPU根据这个值知晓自己是一个中断返回。)

ARM微控制器-MCU基础及CPU运行过程

特征:

ARM微控制器-MCU基础及CPU运行过程

① 中断服务子程虽然会写为一个C语言函数,但是它从来不在主程序代码当中,不由程序代码来调用。

        解释:中断的工作机制是,在主程序中设置好一个中断的发生条件,比如打开全局中断,允许按键中断发生,并且设定好哪一个按键可以触发中断。这样在设定好后,我们按了键,按键通过电路的连接以信号的方式通知CPU这个中断发生了,CPU在获知中断发生后,会自动调用相应的中断服务函数,来完成这个中断。

        所以,中断服务函数并不是在代码里被调用的,它是由一个外部中断事件直接通知CPU,被CPU自动调用的。

CPU在每次调用这个函数的时候,与普通的C语言函数不同,它会做大量的保留现场的入栈和出栈的操作。中断一旦使能,中断函数可能在任一时间点上被调用。(这里可以这么理解:普通编程里写的函数,如果调用它,会在程序的某一句某一行进行调用,所以从程序的流程上讲,它调用的时间点是确定的;而中断函数,它是由硬件进行调用,对于主程序流程来讲,他可能会在任一语句之间,由于外部事件的发生而被调用。所以中断必须具备能够保留现场的条件,方便中断结束返回到开始的位置。)

        所以引出中断函数的一个特点:可以在任一时间点上自动地被调用,由用户、程序的运行来决定。

因为它不被主程序流程调用,它也不必向任何程序传递返回值,所以它必然是一个void类型、void返回值的特殊函数。

        当一个中断信号发生并通知CPU的时候,CPU如何正确地找到这个函数去加以执行? 即中断向量表。

ARM微控制器-MCU基础及CPU运行过程

       每一个中断源在表里都拥有一个表项,表项里的值是对应的服务函数的入口地址(函数的入口地址:指针指向函数第一条指令的地址,高深点讲,函数的函数名在C语言里就是指向函数的第一条指令的地址的指针)。所以CPU在收到中断通知时,只要根据这个表找到该中断函数的入口地址,跳转过去给PC指针寄存器,程序就去执行该函数了。

中断优先级:

ARM微控制器-MCU基础及CPU运行过程

中断嵌套:

ARM微控制器-MCU基础及CPU运行过程

        新发生的中断如果没正在执行的中断的优先级高,就会继续执行完现在的中断,再执行新的中断;如果新中断比正在执行的中断的优先级高,那么就会打断当前的中断,跳转到一个新的中断里。如下图所示,

ARM微控制器-MCU基础及CPU运行过程

中断函数使用全局变量的潜在风险:

假设有一个中断函数要做一些数据的处理(比如按键按了多少次),这个次数应该如何与main函数交换这个值呢?

答:声明一个全局变量,并使用它。但这样就带来了一个潜在风险。比如,在main函数中,拿着一个全局变量去做while循环,而且中断函数里也用了这个全局变量,在中断函数中我们把这个全局变量的功能设置为“发生一次中断,值加1”。如果中断发生在while循环中,打断了while循环,当中断返回时,内存中全局变量的值已经发生了变化。

        全局变量在堆上(内存的顶端),是中断函数和主函数都可以访问的一个地址。但内存中的全局变量是不会被恢复和修改的,所以对全局变量的使用要小心。

11. 复位、时钟、存储器和总线

 (1)复位

计算机系统是怎么从一上电跑到程序上的呢?来看复位的过程。

ARM微控制器-MCU基础及CPU运行过程

        再看一下中断向量表,对于ARM CPU,在上电后,它的默认地址是从00开始的。4个字节一个表项,每个表项可以存一个指针(函数的入口地址)。总计48个表项,前16个(0~15)称为异常(或exception,这个异常不是一种错误,而是指CPU必须响应的特殊的中断,包括复位);剩下32个就是通常意义上的interrupt(也就是ARM CPU留给片内外设/外部设备的中断,包括按键、定时、通讯等,是用户最常用的)。

ARM微控制器-MCU基础及CPU运行过程

ARM微控制器-MCU基础及CPU运行过程

        从表中得知,第二个是reset,即复位的中断向量。在这个表项里存储的函数的入口地址,是该芯片复位以后所取回的第一条指令的执行地址(通俗一点就是,芯片一旦完成复位,CPU会自动从这个位置取一个地址,赋给PC指针,跳转到那里,从第一条指令开始执行)。那么,这个地址填谁呢?在绝大多数嵌入式系统中,开发工具会帮你生成一段startup code(一小段入口函数),这个地址就填该函数的第一条指令的地址。而这个入口函数的最后一句一般就是jump到main函数。

问题:表中编号为0的第一项是什么呢?

        答:堆栈指针向量,可以存一个四字节的地址,堆栈指针寄存器的初始值就存在这里。这样的话在上电复位的时候,CPU自动把前两个项的值一个给堆栈指针寄存器,一个给PC指针寄存器。在上电的瞬间,PC指向了可以执行程序的第一条指令,SR指向了可以使用内存的堆栈的起始地址,就可以开始平稳地运行程序。这个过程是CPU自动完成的。

ARM微控制器-MCU基础及CPU运行过程

(2)时钟

ARM微控制器-MCU基础及CPU运行过程

(3)存储器

RAM相当于计算机上的内存,易失,可以作为堆栈,帮助我们在程序运行当中保存变量、保存函数地址等等。

ROM是基于Flash等工艺生产的,相当于计算机上的硬盘,不易失。

ARM微控制器-MCU基础及CPU运行过程

(4)总线

从CPU来讲,是通过总线与外边的存储器加以连接的。总线实现数据的读写和控制。

ARM微控制器-MCU基础及CPU运行过程

        CPU按照地址总线的视角往外看的话,我们会发现受总线所限,会受到一些约束。

        比如:

地址线:会有固定的位宽,地址线如果是8位,就意味着只能从0000 0000到1111 1111进行编码,能够编出256个地址。对于ARM Cortex M的32位处理器,地址线是32位,对外能访问的地址空间是4G个bit。

数据总线:可以是8bit、16bit、32bit...,意味着在单次的数据访问里,是以单字节、双字节还是四字节进行数据存储。所以ARM Cortex M处理器可以四字节四字节地进行数据的存储访问。

ARM微控制器-MCU基础及CPU运行过程

一个例子:从CPU视角按地址段来看待外围世界。

        前面讲的所有的状态寄存器,都是CPU内部的一个单元,对它们的访问是通过指令直接实现的。而当越过CPU,要看一个片上计算机系统这些存储器的时候,就需要通过地址来访问。芯片手册中一般给出了每一个地址段所对应的片上计算机系统的设备,特别是存储器,一一映射到地址空间里,如下图所示。

ARM微控制器-MCU基础及CPU运行过程

        从CPU往外看,可以发现:从0x0000 0000开始到0x0800 0000这个地址段,是128K的Flash,最上边一小段是非易失的用来保存中断向量表。从0x1FFF F000开始到0x2000 3000是RAM,由4K和16K组成,这当中有一个None(或者叫Reserve),是空白地址。

二. MCU基础总结

ARM微控制器-MCU基础及CPU运行过程

        注意这里说的寄存器指的是CPU外部的寄存器。CPU对它们的访问不再是简单地通过自身的指令加以访问,而是必须通过映射在地址上的地址空间来加以访问。所有的寄存器都映射到了总线上,给它们编上号按照地址进行访问。

        指令的0或1,最终都用来控制一些开关,或者一些功能的选通。而在外设里头,这些外部寄存器映射在总线上,从编程的角度看,这些值(0或1)既是编程语言的值,也是电压上的高电压(3.3V)、低电压(0V),最终体现在电路里来控制外设有序工作。这是寄存器的一个重要概念:它是电路与编程的接口,是映射在地址上的、被CPU访问的、带有存储功能的电路单元。

再来看一看MCU Structure:

ARM微控制器-MCU基础及CPU运行过程

中断向量表:决定了上电复位从哪里执行,决定了中断发生可以从哪里调用程序;

Flash:可以存储自己编写的代码,可以存储编写程序使用到的常量;

RAM:可以想象成它的上部是堆,存储全局变量,下部从底往上使用的话,可以是栈,方便进行函数的调用和中断时内存的开销、出栈/入栈;

外设寄存器组:外设的操作都是在这一块进行的。对于CPU来讲,仍然是通过地址对这些寄存器进行0和1的读写来完成对外设电路的控制。

ARM微控制器-MCU基础及CPU运行过程

        如上图所示,左边是ARM Cortex M的标准地址映射表(外设分为外部外设和ARM自己定义的外设),右图是一个具体型号的CPU的地址映射表(MKL25Z128)。所以当学习另一个厂家的RAM处理器时,它们会具有高度的互通和一致性,另一个互通性就是它们指令集的兼容。

flat memory mode(平坦地址的统一映射):指向一个地址,就是一个确定的功能

ARM微控制器-MCU基础及CPU运行过程

在地址框图的模型里,程序究竟是怎么运行的呢?

ARM微控制器-MCU基础及CPU运行过程

        在中断向量表的最顶端放着PC寄存器和SR寄存器应该赋予的初值,上电后CPU自动把这两个值加载给这两个寄存器,导致PC指向了Flash里的某一条指令(也就是要运行的第一个函数的第一个指令),而SR也指向了内存中的某一个位置(堆栈的起始地址,沿着它往上可以使用堆栈)。之后在时钟的驱动下,指令自动逐条执行。

  • 在Flash里,会不断地执行指令来运行程序;
  • 在程序的跳转中,会使用堆栈来保存函数的返回的地址;
  • 在程序中如果使用了全局变量,会在顶部的堆开始使用内存;
  • 程序如果足够复杂需要完成和物理世界的交互,又会通过地址映射访问外设寄存器,来控制不同的外设模块。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

上一篇:c# – ListView以编程方式添加列显示项目类型


下一篇:c# – 访问Datagridview中的不可见列(WinForms)