首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的云盘
我的搜索
我的记录
我的图片
我的图书
我的笔记
我的音乐
我的影视
我的邮件
我的游戏
Search
1
virtuoso和empyrean alps模拟仿真和混仿教程
359 阅读
2
在IC617中进行xa+vcs数模混仿
300 阅读
3
文档内容搜索哪家强? 15款文件搜索软件横向评测
264 阅读
4
科普:Memory Compiler生成的Register file和SRAM有何区别?
257 阅读
5
通俗易懂的AI知识体系图及其产业链全景图
198 阅读
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
登录
Search
标签搜索
AI
python
Docker
vcs
PyQT
STM32
cadence
linux
systemverilog
EDA
Alist
vscode
uos
package
MCU
C
QT
CXL
sed
sv
bennyhe
累计撰写
387
篇文章
累计收到
33
条评论
首页
栏目
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的云盘
我的搜索
我的记录
我的图片
我的图书
我的笔记
我的音乐
我的影视
我的邮件
我的游戏
搜索到
31
篇与
的结果
2025-08-26
C语言实现面向对象三大特性 : 封装、继承、多态
不知道有多少人去了解过语言的发展史,早期C语言的语法功能其实比较简单。随着应用需求和场景的变化,C语言的语法功能在不断升级变化。虽然我们的教材有这么一个结论:C语言是面向过程的语言,C++是面向对象的编程语言,但面向对象的概念是在C语言阶段就有了,而且应用到了很多地方,比如某些操作系统内核、通信协议等。面向对象编程,也就是大家说的OOP(Object Oriented Programming)并不是一种特定的语言或者工具,它只是一种设计方法、设计思想,它表现出来的三个最基本的特性就是封装、继承与多态。1、为什么用C实现OOP 阅读文本之前肯定有读者会问这样的问题:我们有C++面向对象的语言,为什么还要用C语言实现面向对象呢?C语言这种非面向对象的语言,同样也可以使用面向对象的思路来编写程序的。只是用面向对象的C++语言来实现面向对象编程会更简单一些,但是C语言的高效性是其他面向对象编程语言无法比拟的。当然,使用C语言来实现面向对象的开发相对不容易理解,这就是为什么大多数人学过C语言却看不懂Linux内核源码。所以这个问题其实很好理解,只要有一定C语言编程经验的读者都应该能明白:面向过程的C语言和面向对象的C++语言相比,代码运行效率、代码量都有很大差异。在性能不是很好、资源不是很多的MCU中使用C语言面向对象编程就显得尤为重要。2、所具备的条件 要想使用C语言实现面向对象,首先需要具备一些基础知识。比如:(C语言中的)结构体、函数、指针,以及函数指针等,(C++中的)基类、派生、多态、继承等。首先,不仅仅是了解这些基础知识,而是有一定的编程经验,因为上面说了“面向对象是一种设计方法、设计思想”,如果只是停留在字面意思的理解,没有这种设计思想肯定不行。因此,不建议初学者使用C语言实现面向对象,特别是在真正项目中。建议把基本功练好,再使用。利用C语言实现面向对象的方法很多,下面就来描述最基本的封装、继承和多态。3、封装 封装就是把数据和函数打包到一个类里面,其实大部分C语言编程者都已近接触过了。C 标准库中的 fopen(), fclose(), fread(), fwrite()等函数的操作对象就是 FILE。数据内容就是 FILE,数据的读写操作就是 fread()、fwrite(),fopen() 类比于构造函数,fclose() 就是析构函数。这个看起来似乎很好理解,那下面我们实现一下基本的封装特性。#ifndef SHAPE_H #define SHAPE_H #include <stdint.h> // Shape 的属性 typedef struct { int16_t x; int16_t y; } Shape; // Shape 的操作函数,接口函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y); void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy); int16_t Shape_getX(Shape const * const me); int16_t Shape_getY(Shape const * const me); #endif /* SHAPE_H */这是 Shape 类的声明,非常简单,很好理解。一般会把声明放到头文件里面 “Shape.h”。来看下 Shape 类相关的定义,当然是在 “Shape.c” 里面。 #include "shape.h" // 构造函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y) { me->x = x; me->y = y; } void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) { me->x += dx; me->y += dy; } // 获取属性值函数 int16_t Shape_getX(Shape const * const me) { return me->x; } int16_t Shape_getY(Shape const * const me) { return me->y; }再看下 main.c#include "shape.h" /* Shape class interface */ #include <stdio.h> /* for printf() */ int main() { Shape s1, s2; /* multiple instances of Shape */ Shape_ctor(&s1, 0, 1); Shape_ctor(&s2, -1, 2); printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1)); printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2)); Shape_moveBy(&s1, 2, -4); Shape_moveBy(&s2, 1, -2); printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1)); printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2)); return 0; }编译之后,看看执行结果:Shape s1(x=0,y=1)Shape s2(x=-1,y=2)Shape s1(x=2,y=-3)Shape s2(x=0,y=0)整个例子,非常简单,非常好理解。以后写代码时候,要多去想想标准库的文件IO操作,这样也有意识的去培养面向对象编程的思维。4、继承 继承就是基于现有的一个类去定义一个新类,这样有助于重用代码,更好的组织代码。在 C 语言里面,去实现单继承也非常简单,只要把基类放到继承类的第一个数据成员的位置就行了。例如,我们现在要创建一个 Rectangle 类,我们只要继承 Shape 类已经存在的属性和操作,再添加不同于 Shape 的属性和操作到 Rectangle 中。下面是 Rectangle 的声明与定义:#ifndef RECT_H #define RECT_H #include "shape.h" // 基类接口 // 矩形的属性 typedef struct { Shape super; // 继承 Shape // 自己的属性 uint16_t width; uint16_t height; } Rectangle; // 构造函数 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height); #endif /* RECT_H */ #include "rect.h" // 构造函数 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height) { /* first call superclass’ ctor */ Shape_ctor(&me->super, x, y); /* next, you initialize the attributes added by this subclass... */ me->width = width; me->height = height; }我们来看一下 Rectangle 的继承关系和内存布局:因为有这样的内存布局,所以你可以很安全的传一个指向 Rectangle 对象的指针到一个期望传入 Shape 对象的指针的函数中,就是一个函数的参数是 “Shape ”,你可以传入 “Rectangle ”,并且这是非常安全的。这样的话,基类的所有属性和方法都可以被继承类继承!#include "rect.h" #include <stdio.h> int main() { Rectangle r1, r2; // 实例化对象 Rectangle_ctor(&r1, 0, 2, 10, 15); Rectangle_ctor(&r2, -1, 3, 5, 8); printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r1.super), Shape_getY(&r1.super), r1.width, r1.height); printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r2.super), Shape_getY(&r2.super), r2.width, r2.height); // 注意,这里有两种方式,一是强转类型,二是直接使用成员地址 Shape_moveBy((Shape *)&r1, -2, 3); Shape_moveBy(&r2.super, 2, -1); printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r1.super), Shape_getY(&r1.super), r1.width, r1.height); printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r2.super), Shape_getY(&r2.super), r2.width, r2.height); return 0; } 输出结果:Rect r1(x=0,y=2,width=10,height=15)Rect r2(x=-1,y=3,width=5,height=8)Rect r1(x=-2,y=5,width=10,height=15)Rect r2(x=1,y=2,width=5,height=8)5、多态 C++ 语言实现多态就是使用虚函数。在 C 语言里面,也可以实现多态。现在,我们又要增加一个圆形,并且在 Shape 要扩展功能,我们要增加 area() 和 draw() 函数。但是 Shape 相当于抽象类,不知道怎么去计算自己的面积,更不知道怎么去画出来自己。而且,矩形和圆形的面积计算方式和几何图像也是不一样的。下面让我们重新声明一下 Shape 类:#ifndef SHAPE_H #define SHAPE_H #include <stdint.h> struct ShapeVtbl; // Shape 的属性 typedef struct { struct ShapeVtbl const *vptr; int16_t x; int16_t y; } Shape; // Shape 的虚表 struct ShapeVtbl { uint32_t (*area)(Shape const * const me); void (*draw)(Shape const * const me); }; // Shape 的操作函数,接口函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y); void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy); int16_t Shape_getX(Shape const * const me); int16_t Shape_getY(Shape const * const me); static inline uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); } static inline void Shape_draw(Shape const * const me) { (*me->vptr->draw)(me); } Shape const *largestShape(Shape const *shapes[], uint32_t nShapes); void drawAllShapes(Shape const *shapes[], uint32_t nShapes); #endif /* SHAPE_H */看下加上虚函数之后的类关系图:5.1 虚表和虚指针虚表(Virtual Table)是这个类所有虚函数的函数指针的集合。虚指针(Virtual Pointer)是一个指向虚表的指针。这个虚指针必须存在于每个对象实例中,会被所有子类继承。在《Inside The C++ Object Model》的第一章内容中,有这些介绍。5.2 在构造函数中设置vptr在每一个对象实例中,vptr 必须被初始化指向其 vtbl。最好的初始化位置就是在类的构造函数中。事实上,在构造函数中,C++ 编译器隐式的创建了一个初始化的vptr。在 C 语言里面, 我们必须显示的初始化vptr。下面就展示一下,在 Shape 的构造函数里面,如何去初始化这个 vptr。 #include "shape.h" #include <assert.h> // Shape 的虚函数 static uint32_t Shape_area_(Shape const * const me); static void Shape_draw_(Shape const * const me); // 构造函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y) { // Shape 类的虚表 static struct ShapeVtbl const vtbl = { &Shape_area_, &Shape_draw_ }; me->vptr = &vtbl; me->x = x; me->y = y; } void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) { me->x += dx; me->y += dy; } int16_t Shape_getX(Shape const * const me) { return me->x; } int16_t Shape_getY(Shape const * const me) { return me->y; } // Shape 类的虚函数实现 static uint32_t Shape_area_(Shape const * const me) { assert(0); // 类似纯虚函数 return 0U; // 避免警告 } static void Shape_draw_(Shape const * const me) { assert(0); // 纯虚函数不能被调用 } Shape const *largestShape(Shape const *shapes[], uint32_t nShapes) { Shape const *s = (Shape *)0; uint32_t max = 0U; uint32_t i; for (i = 0U; i < nShapes; ++i) { uint32_t area = Shape_area(shapes[i]);// 虚函数调用 if (area > max) { max = area; s = shapes[i]; } } return s; } void drawAllShapes(Shape const *shapes[], uint32_t nShapes) { uint32_t i; for (i = 0U; i < nShapes; ++i) { Shape_draw(shapes[i]); // 虚函数调用 } }5.3 继承 vtbl 和 重载 vptr上面已经提到过,基类包含 vptr,子类会自动继承。但是,vptr 需要被子类的虚表重新赋值。并且,这也必须发生在子类的构造函数中。下面是 Rectangle 的构造函数。#include "rect.h" #include <stdio.h> // Rectangle 虚函数 static uint32_t Rectangle_area_(Shape const * const me); static void Rectangle_draw_(Shape const * const me); // 构造函数 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height) { static struct ShapeVtbl const vtbl = { &Rectangle_area_, &Rectangle_draw_ }; Shape_ctor(&me->super, x, y); // 调用基类的构造函数 me->super.vptr = &vtbl; // 重载 vptr me->width = width; me->height = height; } // Rectangle's 虚函数实现 static uint32_t Rectangle_area_(Shape const * const me) { Rectangle const * const me_ = (Rectangle const *)me; //显示的转换 return (uint32_t)me_->width * (uint32_t)me_->height; } static void Rectangle_draw_(Shape const * const me) { Rectangle const * const me_ = (Rectangle const *)me; //显示的转换 printf("Rectangle_draw_(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(me), Shape_getY(me), me_->width, me_->height); }5.4 虚函数调用有了前面虚表(Virtual Tables)和虚指针(Virtual Pointers)的基础实现,虚拟调用(后期绑定)就可以用下面代码实现了。uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); }这个函数可以放到.c文件里面,但是会带来一个缺点就是每个虚拟调用都有额外的调用开销。为了避免这个缺点,如果编译器支持内联函数(C99)。我们可以把定义放到头文件里面,类似下面:static inline uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); } 如果是老一点的编译器(C89),我们可以用宏函数来实现,类似下面这样:#define Shape_area(me_) ((*(me_)->vptr->area)((me_)))看一下例子中的调用机制:5.5 main.c #include "rect.h" #include "circle.h" #include <stdio.h> int main() { Rectangle r1, r2; Circle c1, c2; Shape const *shapes[] = { &c1.super, &r2.super, &c2.super, &r1.super }; Shape const *s; // 实例化矩形对象 Rectangle_ctor(&r1, 0, 2, 10, 15); Rectangle_ctor(&r2, -1, 3, 5, 8); // 实例化圆形对象 Circle_ctor(&c1, 1, -2, 12); Circle_ctor(&c2, 1, -3, 6); s = largestShape(shapes, sizeof(shapes)/sizeof(shapes[0])); printf("largetsShape s(x=%d,y=%d)\n", Shape_getX(s), Shape_getY(s)); drawAllShapes(shapes, sizeof(shapes)/sizeof(shapes[0])); return 0; }输出结果:largetsShape s(x=1,y=-2)Circle_draw_(x=1,y=-2,rad=12)Rectangle_draw_(x=-1,y=3,width=5,height=8)Circle_draw_(x=1,y=-3,rad=6)Rectangle_draw_(x=0,y=2,width=10,height=15)6、总结 还是那句话,面向对象编程是一种方法,并不局限于某一种编程语言。用 C 语言实现封装、单继承,理解和实现起来比较简单,多态反而会稍微复杂一点。如果打算广泛的使用多态,还是推荐转到 C++ 语言上,毕竟这层复杂性被这个语言给封装了,你只需要简单的使用就行了。但并不代表,C 语言实现不了多态这个特性。来自微信
2025年08月26日
62 阅读
2 评论
0 点赞
2025-08-22
手把手教你写函数指针与回调函数
嵌入式微处理器2022年01月10日12:03摘要:前面分享了关于指针和结构体使用过程,今天是同系列的函数指针和回调函数。函数指针是指向函数的指针变量。通过函数指针C语言可以实现各种强大的功能与设计方法。而回调函数是函数指针最常见的用途,是C语言的重中之重,也是C语言面试当中的必考知识点和难点。在我们平时开发STM32或者其它单片机时,我们经常都会用到原厂提供的固件库函数,固件库函数中有非常多回调函数。那么,什么是回调函数?回调函数是作为参数传递给另一个函数的函数。接受回调作为参数的函数预计会在某个时间点执行它。回调机制允许下层软件层调用上层软件层定义的函数。应用程序代码和硬件驱动程序之间的交互硬件驱动程序是一个独立的可重用驱动程序,它不了解上面的层(用户应用程序)。硬件驱动程序提供API函数,允许用户应用程序将函数注册为回调。然后,此回调函数由硬件驱动程序作为执行的一部分进行调用。如果不使用回调,就会被编码为直接调用。这将使硬件驱动程序特定于特定的高级软件级别,并降低其可重用性。回调机制的另一个好处是,在程序执行期间可以动态更改被调用的回调函数。一、函数指针函数指针,顾名思义它就是一个指针,只不过它是一个函数指针,所以指向的是一个函数。类比一般的变量指针,指针变量,实质上是一个变量,只不过这个变量存放的是一个地址,在32位单片机中,任何类型的指针变量都存放的是一个大小为4字节的地址。int a; < = > void cal_sum(void); int * p; < = > void (*func_ptr)(void); p=&a; < = > func_ptr= &cal_sum;左边走义变量a,右边定义函数cal_sum;左边定义int指针,右边定义func_ptr;左边赋值指针,右边赋值函数指针;可能这样大家还是不太清楚,我是搞嵌入式单片机的,有本事你在Keil中给我举一个例子啊?可以啊,没问题,请看!#include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ uint8_t (*func_ptr)(uint8_t, uint8_t); /*将函数名赋值给函数指针*/ func_ptr = cal_sum; printf("cal_sum_address =0x%p\r\n", cal_sum); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d + %d = %d\r\n", a, b, cal_sum(a, b)); printf("%d + %d = %d\r\n", a, b, func_ptr(a, b)); while(1) { } }这样写大家应该很熟悉吧,我首先定义了一个函数指针func_ptr,接着将我写得cal_sum函数赋值给了函数指针func_ptr 。然后分别打印函数cal_sum的地址,函数指针func_ptr的地址,以及使用cal_sum计算出来的值,和函数值指针func_ptr计算出来的值。那么,结果是啥样呢?可以发现函数指针func_ptr和cal_sum函数的存储的地址以及他们所计算出来的值是一样的。比如在上面求两个数和的基础上再求两个数的乘积和差,会是啥样的呢?代码是这样的: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } uint8_t cal_sub(uint8_t a, uint8_t b) { return a - b; } uint8_t cal_mul(uint8_t a, uint8_t b) { return a * b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ uint8_t (*func_ptr)(uint8_t, uint8_t); /*将函数名赋值给函数指针*/ func_ptr = cal_sum; printf("cal_sum_address =0x%p\r\n", cal_sum); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d + %d = %d\r\n", a, b, cal_sum(a, b)); printf("%d + %d = %d\r\n\n", a, b, func_ptr(a, b)); /*将函数名赋值给函数指针*/ func_ptr = cal_sub; printf("cal_sub_address =0x%p\r\n", cal_sub); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d - %d = %d\r\n", a, b, cal_sub(a, b)); printf("%d - %d = %d\r\n\n", a, b, func_ptr(a, b)); /*将函数名赋值给函数指针*/ func_ptr = cal_mul; printf("cal_mul_address =0x%p\r\n", cal_mul); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d * %d = %d\r\n", a, b, cal_mul(a, b)); printf("%d * %d = %d\r\n", a, b, func_ptr(a, b)); while(1) { } } 截个图,让大家看得更清楚一点:串口打印结果:指向函数的指针被称作是函数指针。通过函数指针,我们可以灵活的调用各种形式相同,但功能不同的函数这样做大大的增加了代码的灵活程度。1、typedef 函数指针我们在定义一个函数指针时常常会这样写:uint8_t (*func_ptr)(void);比较好理解,但下面这个就不好理解了:typedef uint8_t (*func_ptr) (void); 是不是看着有点懵,因为一般的typedef是这样用的:typedef 原类型 别名用法: #include<stdio.h> typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef uint8_t zhiguoxin; void main() { printf("www.zhiguoxin.cn\n"); printf("微信公众号:果果小师弟\n\n"); zhiguoxin a =10; printf("a=%d\n",a); }使用nodepad++编译一下: 然后,在keil中试验。那这样是啥意思呢? typedef uint8_t (*func_ptr) (void); 这里是把定义了一个别名叫(*func_ptr) (void) 的吗,显然不对,其含义是:上面的例子定义func_ptr是一个函数指针, 函数类型是不带形参, 返回参数是uint8_t。要定义的类型是uint8_t (*)(void),没有输入参数,返回值为uint8_t 的函数指针,定义的别名是func_ptr。在分析这种形式的定义的时候可以这样看:先去掉typedef和别名, 剩下的就是原变量的类型。去掉typedef和func_ptr以后就剩:uint8_t (*)(void)。2.为啥使用typedef定义函数指针答:typedef定义的函数指针类型是比较方便和明了的,因为typedef实际上就是定义一个新的数据类型,typedef有这样的一个作用,就可以用它来定义函数指针类型,这个定义的函数指针类型是能够指向返回值是uint8_t的,并且函数的参数是void类型。这里定义的typedef uint8_t (*func_ptr) (void);;就相当于把uint8_t (*) (void); 定义成了另一个别名 func_ptr了。这个func_ptr就表示了函数指针类型。注意:这里的uint8_t (*) (void);实际上不存在这样的写法,只是为了方便理解,这样的写法是不允许的,也是错误的!这样的写法并不代表是一个类型!C语言真是博大精深!3.函数指针常规定义如果不使用typedef就应该这样定义: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ uint8_t (*func_ptr)(uint8_t, uint8_t); /*将函数名赋值给函数指针*/ func_ptr = cal_sum; printf("%d + %d = %d\r\n", a, b, func_ptr(a, b)); while(1) { } } 在keil中测试:4.函数指针typedef定义如果使用typedef就应该这样定义: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ typedef uint8_t (*func_ptr)(uint8_t, uint8_t); /*声明了一个函数指针变量 pFun*/ func_ptr pFun; /*将这个pFun指向了cal_sum函数*/ pFun = cal_sum; printf("%d + %d = %d\r\n", a, b, pFun(a, b)); while(1) { } }为啥要这样?为啥要使用typedef来定义函数指针?其实这个也是类比结构体的操作,在结构体中我们也常常给结构体起别名。综上所述:定义函数指针就有了两种方法。 /* 方法1 */ uint8_t (*func_ptr)(uint8_t, uint8_t) = NULL; /* 方法2 */ typedef uint8_t (*func_ptr)(uint8_t, uint8_t);; func_ptr pFun = NULL; 函数指针也有两种赋值方法: uint8_t (*func_ptr)(uint8_t, uint8_t) = NULL; /* 方法1 */ func_ptr= &cal_sum; /* 方法2 */ func_ptr= cal_sum;上面两种方法都是合法的。其实函数名就是函数的地址,你将函数名cal_sum赋值给函数指针func_ptr,与将函数的地址&cal_sum赋值给函数指针func_ptr是一样的。同样调用函数也有两种方法: /* 方法1 */ func_ptr(a,b) /* 方法2 */ (*func_ptr)(a,b) 二、回调函数既然函数指针和去普通的指针一样,普通的指针可以作为函数的形参,那么函数指针是不是也可以作为函数的形参呢?答:是的,肯定可以。那么函数指针作为函数的形参我们把这个函数指针叫啥呢?答:回调函数。回调函数原来是这样得来的啊,学到了!能不能举一个简单的例子呢?uint8_t compute_func(uint8_t, uint8_t);首先我们这样写一个函数是没有问题的,但是我们现在要将函数指针作为函数的形参,这样合法吗?uint8_t compute_func(uint8_t (*func_ptr)(uint8_t, uint8_t),uint8_t, uint8_t);编译一下:发现没有错误也没有警告,说明我们把函数指针当做函数的形参是没有任何问题的。在这个函数当中,通过该函数指针调用的函数被称为回调函数。这种开发方式的用途非常广泛。具体来说,在回调函数的应用场景当中,会出现两个角色。分别是某功能函数的开发者以及该功能函数的使用者。compute_func函数就是开发者写的函数,是非常牛逼的写库和底层的那一类人写的函数,我们每一个单片机的使用者,需要写出各种各样的具体的功能函数,只要我们写得功能函数的形参和返回值和函数指针的类型相同就可以了。怎么理解? #include "sys.h" #include "delay.h" #include "usart.h" /*使用者写的函数*/ uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } /*开发者写的函数*/ uint8_t (compute_func)(uint8_t (*func_ptr)(uint8_t, uint8_t), uint8_t a, uint8_t b) { return func_ptr(a, b); } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; printf("compute_func(cal_sum,a,b) =%d\r\n", compute_func(cal_sum, a, b)); while(1) { } } 注意:这里要注意的是我们使用者写的函数的类型一定要于开发者写的回调函数类型一样,比如形参和返回值的类型要一样。不然肯定不能调用的。换句话说就是,下面的这两个函数的形参和返回值都必须是相同的类型才可以,不能一个有返回值一个没有,明明函数指针有两个形参,你写的函数却只有一个形参也是不行的。正确写法:uint8_t cal_mul(uint8_t , uint8_t ) uint8_t (*func_ptr)(uint8_t, uint8_t)错误写法:void cal_mul(uint8_t , uint8_t ) //你写的函数却没有返回值 uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有返回值错误写法:uint8_t cal_mul(uint8_t) //你写的函数却只有一个形参 uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有两个形参我们来验证一下: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" /*使用者写的函数*/ uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } /*使用者写的函数*/ void cal_sub(uint8_t a, uint8_t b) { printf("666"); } /*使用者写的函数*/ uint8_t cal_mul( uint8_t a) { return a; } /*开发者写的函数*/ uint8_t (compute_func)(uint8_t (*func_ptr)(uint8_t, uint8_t), uint8_t a, uint8_t b) { return func_ptr(a, b); } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; printf("%d\r\n", compute_func(cal_sum, a, b)); printf("%d\r\n", compute_func(cal_sub, a, b)); printf("%d\r\n", compute_func(cal_mul, a, b)); while(1) { } } 看到了在keil中编译器不会报错,但是会报警告。因为在keil中编译做了优化。那么,如果我们gcc记事本编译一下,又会是啥样的呢?会发现同样会有两个警告,但还是可以运行的。如何理解回调函数有时候会遇到这样一种情况,当上层人员将一个功能交给下层程序员完成时,上层程序员和下层程序员同步工作,这个时候该功能函数并未完成,这个时候上层程序员可以定义一个API来交给下层程序员,而上层程序员只要关心该API就可以了而无需关心具体实现,具体实现交给下层程序员完成即可(这里的上层和下层程序员不指等级关系,而是项目的分工关系)。这种情况下,就会用到回调函数(Callback Function),现在假设程序员A需要一个FFT算法,这个时候程序员A将FFT算法交给程序员B来完成,现在来让实现这个过程: #include <stdio.h> int InputData[100]={0}; int OutputData[100]={0}; /*定义回调函数*/ void CallBack_FFT_Function(int *inputData,int *outputData,int num) { while(num--) { printf("www.zhiguoxin.cn\r\n"); } } /*用来注册回调函数的功能函数*/ void TaskA(void (*fft)(int*,int*,int)) { fft(InputData,OutputData,5); } int main(void) { /*注册FFT_Function作为回调*/ TaskA(CallBack_FFT_Function); return 0; } 这个例子是不是跟上面的那个例子是相同的,只是我们在这里换了一种说法而已。也就是我们硬件层实现的某个功能,当然可以在应用层直接调用,但这种做法太low了,一看就是小学生的水平,或者说硬件层的东西应用层根本不需要关心,这就是分层的思想。硬件的东西就给硬件工程师做,应用工程师只关心自己的需要实现的任务。这也就是驱动工程师和应用工程师的区别,我硬件工程师只需要写好对应的API函数,你应用层直接调用就好了,你不需要关心这个API函数的内部是怎么实现的。而这两者之间的桥梁就是回调函数。而回调函数的形参就是函数指针,所以本篇最开始讲的是函数指针,只要你函数指针明白了,你就会写回调函数,也就理解了这其中到底只一个什么原理。上面的代码中CallBack_FFT_Function是回调函数,该函数的形参为一个函数指针,TaskA是用来注册回调函数的功能函数。可以看到,用来注册回调函数的功能函数中申明的函数指针必须和回调函数的类型完全相同。函数指针结构体但是,很多时候我们一般在结构体中定义函数指针用的比较多一点。下面,再举一个简单的例子。 #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" /**************************************** * 函数指针结构体 开发者写的结构体 ***************************************/ typedef struct { uint8_t (*p_sum)(uint8_t, uint8_t); uint8_t (*p_sub)(uint8_t, uint8_t); uint8_t (*p_mul)(uint8_t, uint8_t); float (*p_div)(uint8_t, uint8_t); } Operation_T; /*声明结构体变量g_Operation*/ Operation_T g_Operation; /*使用者写的回调函数*/ uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } /*使用者写的回调函数*/ uint8_t cal_sub(uint8_t a, uint8_t b) { return a - b; } /*使用者写的回调函数*/ uint8_t cal_mul( uint8_t a, uint8_t b) { return a * b; } /*使用者写的回调函数*/ float cal_div(uint8_t a, uint8_t b) { return a / b; } /*结构体变量g_Operation初始化*/ Operation_T g_Operation = {cal_sum, cal_sub, cal_mul, cal_div}; int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*使用函数指针调用函数*/ printf("%d\r\n", g_Operation.p_sum(a, b)); printf("%d\r\n", g_Operation.p_sub(a, b)); printf("%d\r\n", g_Operation.p_mul(a, b)); printf("%f\r\n", g_Operation.p_div(a, b)); while(1) { } } 三、回调在嵌入式系统中的实际使用回调可用于多种情况,并广泛用于嵌入式固件开发。它们提供了更大的代码灵活性,并允许我们开发可由最终用户进行微调而无需更改代码的驱动程序。在我们的代码中具有回调功能所需的元素是:将被调用的回调函数cal_sum将用于访问回调函数的函数指针p_sum将调用回调函数的调用函数compute_func在stm32的HAL库中,是使用了大量的回调函数的,串口、定时器等外设都是有对应的回调函数的,回调机制可以更好地分离代码,应用层和驱动层完全分离,降低耦合性。简单来看几个例子,串口回调函数:void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);使用的时候,我们只需要把串口解析处理逻辑放在对应的回调函数中处理即可,拿串口接收来举例,定义的是一个弱函数,我们在自己的文件中重新实现就好。 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { /*****Data Processing********/ } else if (huart->Instance == USART2) { /*****Data Processing********/ } } 又比如,我们在OS中的创建任务的函数,就是一个用来注册回调函数的功能函数,如果我们需要创建一个任务只需要这样写,tcp_client_socket就是我们底层开发者要写的具体的回调函数,就算你底层开发者没有把这个tcp_client_socket函数写完,对我程序的大体框架是没有任何影响的。我不管你底层开发者是否会不写,我的任务完成了。相反我底层应用开发者只要把我底层函数写好就行了,不管写没写好,我只需要把这个tcp_client_socketAPI函数名给你就行了,其他的你也不需要管了,咋们井水不犯河水!static void tcp_client_socket(void *thread_param) { /*****Data Processing********/ }总结指针、结构体、函数指针、回调函数都是在嵌入式开发中非常重要的知识点,也是不好理解的,但如果能够搞清楚它们是怎么来的,对嵌入式的进阶是非常有帮助的,所谓基础不牢,地动山摇。遇到问题一定要刨根问题。搞清楚原理,动手写代码来验证自己的思维过程,就一定能搞明白!来自微信
2025年08月22日
23 阅读
0 评论
0 点赞
2025-08-13
大一新生开源 Python 脚本可视化工具,火了!
Python数据科学2021年01月25日14:05普普通通黑底白字地敲代码太枯燥?那么,把 Python 脚本可视化怎么样?就像这样,从输入图片、调整尺寸到双边滤波,每一步都能看得清清楚楚明明白白。输入一个矩阵,无论是对它进行转置、求共轭还是乘方,都能得到及时的反馈。这样一个 Python 脚本可视化工具,名叫 Ryven,出自一位名叫 Leon Thomm 的大一新生之手。如果你也想试用一番,不妨接着往下看。如何使用使用之前,简单准备一下开发环境:Python3(推荐 3.8 以上)PySide2(推荐 2.14 以上)运行该项目中的 Ryven.py,即可打开编辑器。作者提到,在 Ryven 中,有许多不同的脚本。每个脚本都包含变量、流(或图)以及日志。右键单击就可以轻松操作。不过,想要使用这些节点,需要先导入节点软件包。点击 file,选择 import Nodes,导入 *.rpc 文件即可。这个工具的另外一个特点是,包含了两种不同的算法模式。数据流:每次数据更改(节点数据输出也就更改了)都会向前传播,并在所有连接的节点中触发更新。如下图,滑动滑块,会立即触发右侧节点中结果的更新。执行流:数据不会在更改时立即向前传播,而是只会在某个节点请求输出数据时,在受影响的节点中触发更新。另外,作者还给自己列了一个 to do list,比如加入语法高亮功能、自动代码补全功能,完善视觉体验等等。可视化编程是不是还挺有趣的?不过,作者小哥也指出,可视化编程的目的不是取代文本编程,并且,文本编程中实现的许多工作没有可视化的必要。关于作者作者 Leon Thomm,目前是苏黎世联邦理工学院的大一学生,自称 “业余程序员”,致力于人机交互和可视化编程。根据他的个人简介,早在进入大学前,他已经具备丰富的软件开发经历。传送门项目地址:https://github.com/leon-thomm/Ryven来自微信
2025年08月13日
40 阅读
0 评论
0 点赞
2025-08-13
答应我,调试 Python 代码,不要再用 Print 了!
Python数据科学2021年01月27日14:27相信大部分人学习Python,肯定会用print()这个内置函数,来调试代码的。那么在一个大型的项目中,如果你也是使用print来调试你的Python代码,你就会发现你的终端有多个输出。那么你便不得不去分辨,每一行的输出是哪些代码的运行结果。举个例子,运行下面这个程序。num1 = 30num2 = 40 print(num1)print(num2)输出结果。3040这些输出中哪一个是num1?哪一个又是num2呢?找出两个输出可能不是很困难,但是如果有五个以上的不同输出呢?尝试查找与输出相关的代码可能会很耗时。当然你可以在打印语句中添加文本,使其更容易理解:num1 = 30num2 = 40 print("num1" num1)print("num2" num1)输出结果。num1 30num2 40这个结果就很容易理解了,但是需要时间去写相关的信息。这时就该「Icecream」上场了~什么是Icecream?Icecream是一个Python第三方库,可通过最少的代码使打印调试更清晰明了。使用pip安装Icecream库。pip install icecream下面,让我们通过打印Python函数的输出来进行尝试。from icecream import ic def plus_five(num): return num + 5 ic(plus_five(4)) ic(plus_five(5))输出结果如下。ic| plus_five(4): 9ic| plus_five(5): 10通过使用icecream,我们不仅可以看到函数输出,还可以看到函数及其参数!检查执行情况如果你想要找到执行代码的位置,可以通过执行如下所示的操作,来查找执行了哪个语句。def hello(user:bool): if user: print("I'm user") else: print("I'm not user") hello(user=True)输出结果。I'm user使用icecream则无需多余的文本信息,就可以轻松地完成上述的操作。from icecream import ic def hello(user:bool): if user: ic() else: ic() hello(user=True) 输出结果如下。ic| ice_1.py:5 in hello() at 02:34:41.391从输出结果看,函数hello中的第5行的代码已被执行,而第7行的代码未执行。自定义前缀如果您想在打印语句中插入自定义前缀(例如代码执行时间),icecream也是能实现的。from datetime import datetime from icecream import ic import time from datetime import datetime def time_format(): return f'{datetime.now()}|> ' ic.configureOutput(prefix=time_format) for _ in range(3): time.sleep(1) ic('Hello') 输出结果如下。2021-01-24 10:38:23.509304|> 'Hello'2021-01-24 10:38:24.545628|> 'Hello'2021-01-24 10:38:25.550777|> 'Hello'可以看到代码的执行时间,就显示在输出的前面。获取更多的信息除了知道和输出相关的代码之外,你可能还想知道代码执行的行和代码文件。在ic.configureOutput()中,设置includeecontext的参数值为True即可。from icecream import ic def plus_five(num): return num + 5 ic.configureOutput(includeContext=True) ic(plus_five(4)) ic(plus_five(5))输出结果如下。ic| ice_test.py:7 in - plus_five(4): 9ic| ice_test.py:8 in - plus_five(5): 10这里我们就知道了,第一个输出是由函数plus_five在文件icecream_example.py的第7行执行的。第二个输出则是由函数plus_five在代码文件的第8行执行的。上述两个操作都用到了ic.configureOutput()函数。通过查看源码,可知有四个可供设置的参数。prefix,自定义输出前缀outputFunction,更改输出函数argToStringFunction,自定义参数序列化字符串includeContext,显示文件名、代码行、函数信息删除Icecream代码最后你可以将icecream仅用于调试,而将print用于其他目的(例如漂亮的打印)。from icecream import ic def plus_five(num): return num + 5 ic.configureOutput(includeContext=True) ic(plus_five(4)) ic(plus_five(5)) for i in range(10): print(f'****** Training model {i} ******') 输出结果。ic| ice_1.py:7 in - plus_five(4): 9ic| ice_1.py:8 in - plus_five(5): 10 Training model 0 ** Training model 1 ** Training model 2 ** Training model 3 ** Training model 4 ** Training model 5 ** Training model 6 ** Training model 7 ** Training model 8 ** Training model 9 **由于你可以区分调试打印和漂亮打印,因此搜索和删除所有ic调试语句非常容易。删除所有调试代码后,你的Python代码就整洁了。总结到此,你就应该就学会了如何使用icecream去打印调试。更多功能可以访问「GitHub」,了解详情~https://github.com/gruns/icecream来自微信
2025年08月13日
29 阅读
0 评论
0 点赞
2025-08-13
存储器芯片行业研究宝典
硬科技精品投行的驭势资本2021年01月22日19:01储存器中国存储器芯片行业定义及分类DRAM、NOR Flash、NAND Flash三类存储器之间的应用已产生隔离,难以相互代替,市场自成体系。存储器芯片定义及分类存储器芯片是半导体存储产品的核心,是电子系统中负责数据存储的核心硬件单元,其存储量与读取速度直接影响电子设备性能。半导体存储按照掉电后是否保存数据,分为易失性存储和非易失性存储。易失性存储主要以随机存取器RAM为主,使用量最大的为动态随机存储DRAM。非易失性存储中最常见的为NOR Flash与NAND Flash,其中NOR Flash因其读取速度快且可擦除写入,被作为代码存储的主要器件,NAND Flash在高容量时具有成本优势,且读写速度比传统的光学、磁性存储器快,是现在主流的大容量数据存储器件。中国存储器芯片行业制程分析当前中国NOR Flash芯片技术基本成熟,但在DRAM、NAND Flash芯片领域,仍与国际领先水平有着一代以上的技术差异。中国存储器芯片行业市场现状存储器芯片传统应用市场规模稳定,近年来,随着技术的发展,不断有新下游应用拉动行业发展。新应用市场数据存储需求汽车电子系统开始支持GUI、语音识别、高级数据处理功能产生大量数据存储需求。口令存储需求随着汽车智能化发展,搭载更多即时启动应用,而及时启动最佳解决方案为NOR Flash。产业链存储芯片产业是国家战略产业,直接关系到电子信息产业的发展,中国正逐渐在全产业链各个环节中实现对进口产品的替代。存储器芯片产业链介绍中国半导体产业链由上游为半导体支撑产业,中游为存储芯片行业,下游市场参与者由众多电子整机厂组成。存储器芯片是集成电路价值量最大的产品之一,存储芯片产业是国家战略产业,直接关系到电子信息产业的发展,中国正逐渐在全产业链各个环节中实现对进口产品的替代。产业链上游分析大基金二期注册成立,以长江存储为代表的存储器芯片厂商是重点投资对象,其产业链上游的半导体材料与设备是投资热点。存储器芯片行业产业链上游分析大基金二期重点布局半导体产业链上游,半导体材料与半导体设备行业有望在未来实现进口替代。2016年成立的大基金一期接近尾声,其重点投资领域为集成电路制造,重点解决中国晶圆代工产能不足、技术落后的问题。2019年10月,大基金二期注册成立,以长江存储为代表的存储器芯片厂商是重点投资对象,其产业链上游的半导体材料与设备是基金投资的热点之一。半导体材料技术垄断:美国、日本、韩国、德国等国家占据主导地位中国半导体材料的市场规模占全球比重逐年上涨整体表现为企业数量少、市场规模小、技术水平低以及产业布局分散的特征半导体设备总体国产化率较低,属于产业链薄弱环节,国产替代空间巨大中国晶圆厂建设与扩产招标过程中,半导体设备国产化率从逐渐提高存储器芯片发展扩产为中国半导体设备厂商提供了更多的发展机遇,中国将进入半导体设备国产化窗口期产业链中游分析全球范围内,美、韩两国存储器芯片厂商居头部,技术领先,议价能力强,近年来中方企业技术逐渐实现赶超,预计未来将实现国产替代。设计环节(占成本30%)1、中国IC设计行业缺乏自主设计流程的能力,还不具备COT设计能力,主要依靠工艺技术的进步和EDA工具的进步。2、除兆易创新外,中国存储器芯片厂商多为IDM模式发展。制造环节(占成本40%)1、当前在高端制程,中国厂商难以实现国产替代。2、3D NAND Flash领域:三星86层技术成熟,当前长江存储64层产品已小范围量产,目前在调试设备跨86层。3、实现128层技术弯道超越。4、DRAM领域:当前中国全面落后于国际头部企业。封测环节(占成本30%)1、中国集成电路封测水平居全球领先水平,已完全实现国产替代。2、存储器芯片封测行业属于劳动密集型、技术密集型企业。3、封测水平反向推动产业链中游芯片制造业的发展。产业链下游分析三大主流存储器芯片近年来下游市场规模逐年扩大,旺盛的下游需求推动存储器芯片行业的发展。电子整机搭载内存容量不断扩大•PC市场:需求从装机标配4GB过渡到了8GB、16GB甚至是32GB,市场需求量进一步扩大。•移动端:以智能手机为主要代表的移动端以内存容量作为产品属性提升的空间,当手机内存的标配从1GB、2GB转变到6GB、8GB时,其对DRAM的需求量也有了极大的增长,再加上智能手机的快速普及与其巨大的市场保有量,抢占了一大部分DRAM资源。SSD和智能手机市场NAND Flash需求的增长已经弥补了其他消费类电子市场需求的相对平淡。•智能手机:2019年全球智能型手机出货14.9亿台,苹果、三星、华为、OPPO、vivo等头部智能手机品牌旗舰机纷纷以64GB、128GB、256GB为主打容量,再加上平板、车载、智能盒子等细分市场需求eMMC/eMCP等嵌入式产品消耗了42%的NAND Flash产能。•SSD市场:数据中心、服务器等领域对数据分析、处理、响应速度的要求不断提高,谷歌、Facebook、百度、阿里巴巴、腾讯、华为等对SSD需求强劲。消费类市场,超极本、二合一等轻薄笔记本对SSD搭载率不断增加,去年消费类市场SSD出货超1.5亿台,再叠加工业、金融、车载等领域SSD需求,全球SSD共消耗近50%NAND Flash产能NOR Flash下游需求中,除了传统电脑、智慧型手机、网路通讯与消费性电子产品外,近年来最新且成长最大的需求在于智慧型手机的AMOLED屏幕,及LCD驱动IC和TDDI(Touch Display Driver IC)方案。•智能手机:智能手机的AMOLED屏幕需要大量消耗NOR Flash颗粒•随着物联网、可穿戴设备、智慧城市、智慧应用、智能家居、智能汽车、无人机等厂商使用NOR Flash作为储存装置和微控制器搭配开发,NOR Flash需求将呈现爆发性增长。市场规模存储器芯片应用广泛,随着5G、物联网技术为中国半导体行业发展赋能,未来市场规模将进一步扩大。近五年来,受PC及移动端电子设备内存容量不断扩大,以TWS为代表的可穿戴设备新型消费级市场快速扩张,以及大数据云计算技术不断释放对企业级存储的需求等多方因素的影响,中国存储器芯片行业整体不断发展,市场规模(以销售额计)从2014年的45.2亿美元增长到了2019年123.8亿美元,年复合增长率高达28.6%。由于当前存储器芯片应用广泛,同时下游消费电子市场份额逐年扩大,且未来5G及物联网技术将进一步为中国存储器芯片的整体发展赋能,预计未来中国存储器芯片还将继续保持稳定增长的态势。到2024年,中国存储器芯片市场份额有望突破522.6亿美元,占全球市场的14%。市场规模预测逻辑中国电子整机制造业反向驱动上游存储器芯片发展,中国存储器芯片市场增速高于全球增速。储存器中国存储器芯片行业驱动因素国产替代大环境助推中国存储器芯片厂商有天然地缘优势,未来对进口的依赖将会进一步减弱,国产替代率将进一步提高。存储器芯片国产替代率逐渐提高近十年来,中国电子工业占全球的比重持续增加,全球80%的电子整机制造在中国大陆完成。受制于中国相对落后的半导体水平,中国集成电路进口持续维持高位。过去五年来,中国集成电路进口数量始终维持上涨的趋势。2018年,中国集成电路进口金额与进口数量分别高达3,120.6亿美元,4,175.7亿只。受制于中国集成电路行业起步较晚,行业技术水平整体落后于西方发达国家,短时间内集成电路进口数量与进口金额仍将维持高位。美国限制对中国的科技技术出口,长期将加速半导体国产化进程。目前,中国在生产代工、设备、存储器、计算、模拟及数模转换芯片、射频前端、EDA软件等领域缺口较大,存在进口替代机会。存储器芯片品牌化程度较弱存储器芯片产品具有典型的大宗商品属性,差异化竞争较小,不同企业生产的产品技术指标基本相同,标准化程度较高,因此品牌化程度较弱,用户粘性低。从电子整机下游消费者角度考量:消费者通常只会考虑存储芯片的容量,如手机存储量是64G还是128G,对存储器芯片品牌不会有过多关注。从存储器芯片厂角度考量:行业壁垒高,头部企业通常体量大、投资高、规模庞大,下游整机厂在选配存储器芯片时,在产品性能、物理属性等技术性能接近的情况下,报价通常作为第一考量因素。从电子整机厂角度考量:尤其是消费类电子整机出货量通常以亿为计量单位,存储器芯片作为核心存储硬件单元,需求量与其倍数相关,巨量需求下,性价比直接决定品牌的市场份额。因此,对于存储器芯片行业,只要技术参数上达到产品需求,不同品牌的可替代率很高,这为中国存储器芯片品牌的发展提供了弯道超车的可能。物联网技术的发展物联网技术的发展使得设备的网络接入量与整体数据存储量呈现爆发式增长,直接拉动存储器芯片行业的发展。物联网是NOR Flash发展的核心推动力物联网技术的发展使近年来NOR Flash呈现市场复苏。通常,物联网接入设备的系统与手机、计算机等相比更简单,处理数据更少,对存储空间的要求较少,一般在几兆到几百兆之间。此时物联网接入设备采用NOR Flash替代传统计算机、手机等设备以DRAM和NAND Flash为核心的内存处理方案是性价比最高的选择,这使得物联网技术赋能的新设备仍能维持在当前价格水平,逐渐提高在整体产品市场的渗透率。物联网技术发展对存储器芯片行业推动作用物联网将更多常见设备接入互联网,如冰箱、空调、洗衣机、电视等。移动端电子产品及可穿戴设备市场规模的不断增大。存储空间增大提升单颗芯片售价。物联网、云计算等新增应用叠加5G基建产生巨量数据,需要更强算力更大存储量服务器支持。物联网技术赋予电子产品更强功能,需要更大内存空间支持。中国存储器芯片行业风险因素分析如现阶段全球疫情得不到有效控制,下游整机产能下降,势必对上游存储芯片行业造成产能难以爬坡与库存周期延长的负面影响。“黑天鹅”影响全年消费2020年一季度是电子消费产品发布新品旺季,在“新冠疫情”与“全球油价下跌”两只黑天鹅冲击下,2月中国制造业PMI降至35.7%,为近十五年来最低。因电子产品产业链覆盖面广,参与者众多,受不同环节、不同零部件复工复产进度不均的影响,大部分电子整机OEM厂年后产量爬坡受阻,以代工厂富士康为例,大陆生产受阻致使其母公司营收环比下降40%,创下八年新低。消费电子终端市场新品上市后产能供应不足,且市场消费信心不足。据中国信息通信研究院数据,2月中国手机市场总出货量638.4万部,同比下降56%,国产品牌手机出货量1,310.8万部,同比下降14.7%,5G手机238万部,占比37.3%,包括华为、小米、OPPO、vivo等品牌厂均受到了不同程度的影响,且未来几个月还将受到海外“疫情”的影响。整机厂产能下降对上游半导体行业景气程度产生消极影响受疫情影响,各地节后复工情况步调不一致,整机厂面临一系列疫情带来的制约因素:•供应链上下游延迟复工,或将延期投产;•物流速度降低,甚至可能出现停运;•产品入关检查的时间和财务成本或增加,为海外销售带来更高挑战。全球20%晶圆代工产能落地中国,其主要原因是靠近下游客户。虽然中国消费电子产品全链条制造资源的丰富和完善程度全球领先,仍需警惕整机厂行业寒冬期对产业链的冲击。“新冠疫情”本年度对存储器芯片产业影响全球电子产品市场消费信心下降韩国受疫情影响较重,存储器芯片出货量或将受到影响欧美日受疫情影响严重,半导体材料及设备供应受到影响制造业三大周期:产品周期、资本开支/产能周期、库存周期。产品周期是所有周期的根本,也是最长的周期。在存储芯片领域,产品周期代表的是最核心最根本的影响因素,即下游需求驱动力。如PC、手机、TWS耳机是半导体行业发展过程中的产品周期,手机周期也根据技术迭代带来的产品周期进一步细分为3G、4G、5G。1、如疫情得不到有效控制,下游整机产能下降,势必对上游存储芯片行业造成产能难以爬坡与库存周期延长的负面影响。2、同时,存储芯片如未能在二季度实现产能顺利爬坡,也会造成库存周转困难,影响产品周期健康有序向上发展的势头。储存器中国存储器芯片行业相关政策法规存储器芯片作为重要的分立器件细分应用领域,其行业的稳定发展与中国分立器件的整体发展密切相关。集成电路在电子信息产业的地位促使国家近二十年来不断出台政策鼓励行业发展,其中最直接的政策是2011年《国务院关于印发进一步鼓励软件产业和集成电路产业发展若干政策的通知》中明确对IC设计和软件企业实施所得税“两免三减半”优惠政策,该政策一直延续至今。2019年5月22日,财政部、税务总局发布公告,为支持IC设计和软件产业发展,依法成立且符合条件的IC设计企业和软件企业,在2018年12月31日前自获利年度起计算优惠期,第一年至第二年免征企业所得税,第三年至第五年按照25%的法定税率减半征收企业所得税,并享受至期满为止。此前,国常会就决定延续集成电路企业所得税优惠政策,会议决定,在已对集成电路生产企业或项目按规定的不同条件分别实行企业所得税“两免三减半”或“五免五减半”的基础上,继续实施2011年明确的所得税“两免三减半”优惠政策。中国存储器芯片行业发展趋势存储芯片迎来黄金发展期全球存储器市场从去年的供过于求演变到下半年及明年的供不应求,存储芯片价格上涨将超10%。5G手机增加存储器用量未来几年全球5G手机激活市场会从2019年的近1,000万台,爆增到2020年的1.6-2.0亿及2021年的4.0-5.0亿台,而每台5G手机都需配备8GB或以上的mobile DRAM及128-256GB的NAND闪存。与4G手机配备64-128GB的NAND Flash相比,预计手机用NAND Flash于2020-2021年增长率超30%。云服务器市场需求量的复苏受疫情影响,春节期间云端服务器客户量急剧上涨。在线医疗、在线娱乐、在线教育、在线买菜等云业务的普及使得服务器数量及服务器内存用量急剧增长。近年来,云端服务器用户大幅增长,服务器用DRAM占整体DRAM用量比例逐年上涨。由于英特尔在推出14nm++ Cooper Lake及10nm+ Ice Lake CPU数据通路自6拓展为8,数据处理效率更高,可以搭载更多内存单元,服务器内存芯片的用量将显著增加,据专家预测,2021年服务器用DRAM芯片用量占整体DRAM用量比例将达38%。中国存储芯片国产替代还有较大的发展空间全球内存及闪存产品在国际竞争市场上,基本均被韩国、日本、美国等国垄断。在DRAM领域,三星、海力士及美光为行业龙头,在NAND领域,三星、东芝、新帝,海力士以及美光、英特尔共同掌握全球话语权。当前,中国已初步完成在存储芯片领域的战略布局,但由于中国起步晚,且受到技术封锁,市场份额较少,距离全面国产替代还有较大的发展空间。存储芯片良好的发展态势将为中国在这一领域的发展提供源源不断的需求保障。IP创新与自主制造存储器的IP集中度低,面对国际技术封锁,当前中国厂商主要通过合作授权与自主研发相结合的方式获得IP版权。IP创新与自主制造是存储器芯片的两个发展方向对于存储器芯片,由于存储器芯片制程的难点在于IP和制造,头部厂商的主流经营模式为IDM模式,受制于欧美日韩对中国半导体行业的限制,中方获得IP的主要方式为合作授权与自主研发相结合的方式。DRAM的IP领域由于在DRAM领域中国厂商总体起步较晚,专利积累相对薄弱。但由于DRAM总体来说技术发展相对成熟,国际领先企业在研发领域资本投入已有所减少,这为中国厂商继续提高资本投入已实现国产替代提供了良好的机会。在此基础上中国厂商加快IP自主研发,降低成本的同时提高产品性能,从而在议价能力及定价弹性达到国际领先水平。NAND FLASH的IP领域NAND Flash的IP方面,3D NAND Flash堆叠技术自2D平面技术升级而来,由于3D堆叠技术为近年来出现的新技术,中国头部企业长江存储与国际大厂的技术差距相对较小。但在IP储备领域,中国厂商仍处于弱势地位,三星、东芝、闪迪、海力士等存储器芯片巨头厂商仍具有压倒性优势。制造领域在半导体产业向中国转移的大趋势下,国际大厂纷纷在大陆地区设厂或增大中国大陆建厂规模。据SEMI数据显示,近四年来全球投产晶圆厂超60座,其中26座位于中国大陆,占全球晶圆厂比例超40%。制造业是集成电路的核心环节,制造环节向大陆的迁移直接促进中国存储器芯片产业的发展。随着大量晶圆厂在中国的建成,中国存储器芯片将迎来先进制程技术的突破与成熟。储存器中国存储器芯片行业竞争格局当前中国基本实现NOR Flash芯片的进口替代,但在DRAM、NAND Flash芯片领域,仍与国际领先水平有不小差距。中国存储器芯片行业国产替代潜力大全球存储器芯片市场规模大且竞争激烈,当前中国已基本实现NOR Flash芯片的进口替代,但在DRAM、NAND Flash芯片领先制程领域,仍与国际领先水平有不小差距。DRAM发展道阻且长中国大陆是全球DRAM最大市场,但自给率几乎为0。现阶段,半导体产业中心已转移到中国大陆,中国大陆已是全球最大和增速最快的市场,但大陆半导体产业起步晚,自给率仅为15%左右。DRAM作为半导体和存储器最大细分市场,2018年占据全球半导体和存储器总产值的比例分别为22%和58%,中国大陆作为最大市场,销售额全球占比约为43%,但几乎完全依赖进口,自制率远低于半导体全行业水平。NAND Flash发展初步取得成果三星、海力士、东芝、西部数据、美光、英特尔等巨头在产能上持续投入。2018年,64层、72层的3D NAND闪存已成业界主力产品,2019年开始量产92层、96层的产品,到2020年,大厂们即将进入128层3D NAND闪存的量产。长江存储64层三维闪存产品的量产有望使中国存储芯片自产率从8%提升至40%。在美日韩大厂垄断下,长江存储的64层3D NAND闪存量产消息别具意义。储存器中国存储器芯片行业投资企业推荐武汉新芯武汉新芯为存储器芯片龙头企业,专注于NOR Flash与晶圆级XtackingTM技术,是紫光集团旗下核心企业。主营业务武汉新芯集成电路制造有限公司(以下简称“武汉新芯”),于2006年在武汉成立,是一家领先的集成电路研发与制造企业。专注于NOR Flash与晶圆级XtackingTM技术,致力于为全球客户提供高品质的创新产品及技术服务。作为紫光集团旗下核心企业,武汉新芯将整合集团和产业链合作伙伴的资源,并充分利用自身优势,努力为其全球客户提供高性能、高可靠性、低功耗、高性价比的产品和解决方案。公司产品•NOR FALSH代工武汉新芯自2008年开始向客户提供专业的300MM晶圆代工服务,在NOR Flash领域已经积累了十多年的制造经验,是中国乃至世界领先的NOR Flash晶圆制造商之一。•XtackingTM武汉新芯自主研发、国际先进的晶圆级三维集成技术平台XtackingTM。公司是国内首家采用硅通孔技术(TSV)来生产图像传感器的制造商,已积累了多年的大规模量产经验,产品集高性能、低功耗、高集成度的优点于一体,广泛应用于中国智能手机市场。投资亮点技术领先XtackingTM技术平台已推出硅通孔技术(TSV)、混合键合( Hybrid Bonding)和多片晶圆堆叠技术(Multi-Wafer Stacking),为客户提供极具灵活性和创新性的晶圆级三维集成技术解决方案。质量可靠武汉新芯一直严格遵守质量管控和环境、安全、健康管理体系,并获得如汽车行业质量管理体系IATF16949、质量管理体系ISO9001等国际体系认证。战略定位进口替代武汉新芯建设的12英寸芯片项目在2008年正式投产,产品良率达到世界领先水平,结束了中国中部无“芯”的历史。在NOR Flash领域,武汉新芯达到世界领先水平。IDM2017年武汉新芯开始聚焦IDM发展战略,发布了集产品设计、晶圆制造与产品销售于一体的自主品牌,致力于开发高性价比的SPI NOR Flash产品长江存储长江存储是一家专注于3D NAND闪存芯片设计、生产和销售的IDM存储器公司,致力于成为全球领先的NAND闪存解决方案提供商。企业概况长江存储科技有限责任公司(以下简称“长江存储”),总部位于武汉,是一家专注于3D NAND闪存设计制造一体化的IDM集成电路企业,同时也提供完整的存储器解决方案。长江存储为全球合作伙伴供应3D NAND闪存晶圆及颗粒,嵌入式存储芯片以及消费级、企业级固态硬盘等产品和解决方案,广泛应用于移动通信、消费数码、计算机、服务器及数据中心等领域。投资亮点长江存储进入到3D FLASH领域之前,中国一直没有大规模存储芯片的生产,未来,随着云计算、大数据的发展,人类对数据存储要求是越来越高,三维闪存存储芯片是高端芯片一个重要领域,其量产也标志着中国离国际先进水平又大大跨近一步,把中国产品水平跟海外的先进水平缩短到了一代。主营业务长江存储专注于3D NAND闪存晶圆及颗粒,嵌入式存储芯片以及消费级、企业级固态硬盘等产品和解决方案,广泛应用于移动通信、消费数码、计算机、服务器及数据中心等领域。领先产品•2017年10月,长江存储通过自主研发和国际合作相结合的方式,成功设计制造了中国首款3D NAND闪存•2019年9月,搭载长江存储自主创新Xtacking架构的64层TLC 3D NAND闪存正式量产•目前,长江存储正跨越96层制程弯道追赶国际领先128层制程发展战略•长江存储实行纵向一体化的经营模式,具备识别客户需求、进行产品设计、准备原辅材料、封测、销售及持续服务全产业链经营能力。可基于客户需求实现定制化芯片制造长江存储64层三维闪存是全球首款基于Xtacking架构设计并实现量产的闪存产品,拥有同代产品中最高存储密度。•创新的Xtacking技术只需一个处理步骤就可通过数十亿根垂直互联通道(VIA)将两片晶圆键合,相比传统三维闪存架构可带来更快的传输速度、更高的存储密度和更短的产品上市周期。长鑫存储长鑫存储专业从事DRAM存储器芯片的研发、生产和销售,目前已建成第一座12英寸晶圆厂并投产。企业简介合肥长鑫存储技术有限公司(以下简称“长鑫存储”),事业开始于2016年。长鑫存储专业从事动态随机存取存储芯片(DRAM)的研发、生产和销售,目前已建成第一座12英寸晶圆厂并投产。DRAM产品广泛应用于移动终端、电脑、服务器、人工智能、虚拟现实和物联网等领域,市场需求巨大并持续增长产品介绍长鑫存储的8Gb LPDDR4规格的DRAM芯片已经投片,相较于上一代DDR3内存芯片,DDR4内存芯片拥有更快的数据传输速率、更稳定的性能和更低的能耗。长鑫存储自主研发的DDR4内存芯片满足市场主流需求,可应用于PC、笔记本电脑、服务器、消费电子类产品等领域。高速数据传输多领域应用支持可靠性保障主流市场需求匹配多产品组合战略定位12寸晶圆制造母公司兆易创新宣布与合肥市产业投资控股集团签署合作协议,以长鑫存储为载体研发19纳米制程的12寸晶圆DRAM,总预算为人民币180亿元。国产替代预计将在全球DRAM市场占得约8%的市场份额填补国产DRAM存储器在本国市场的空白投资亮点自主研发长鑫存储聚集了集成电路行业的领袖,研发、设计及制造的专家,以及国际化的经营管理团队,不断创新,积极培养本地优秀人才,是具有尖端技术开发能力和工艺制造能力的“中国创造”模范企业。技术领先长鑫存储也是全球第四家DRAM产品采用20纳米以下工艺的厂商。另外三家是目前DRAM存储的三大巨头,三星、SK海力士、美光。这三家的DRAM全球市占率超过95%。参考文献来自:头豹、驭势资本研究所END来自微信
2025年08月13日
37 阅读
0 评论
0 点赞
1
2
3
4
...
7