SEO基础

SEO基础

Products

当前位置:首页 > SEO基础 >

如何设计一个适用于全屏浏览的旅游网站?

96SEO 2026-02-23 14:05 0


先说一说这个电路的用途#xff1a;当两个MCU在不同的工作电压下工作

如何设计一个适用于全屏浏览的旅游网站?

下面这个“电平转换”电路理解后令人心情愉快。

电路设计其实也可以很有趣。

先说一说这个电路的用途当两个MCU在不同的工作电压下工作如MCU1

工作电压5VMCU2

与MCU2之间怎样进行串口通信呢很明显是不能将对应的TX、RX引脚直接相连的否测可能造成较低工作电压的MCU烧毁

下面的“电平双向转换电路”就可以实现不同VDD芯片工作电压的MCU之间进行串口通信。

该电路的核心在于电路中的MOS场效应管2N7002。

他和三极管的功能很相似可做开关使用即可控制电路的通和断。

不过比起三极管MOS管有挺多优势后面将会详细讲起。

下图是MOS管实物3D图和电路图。

简单的讲要让他当做开关只要让Vgs导通电压达到一定值引脚D、S就会导通Vgs没有达到这个值就截止。

那么如何将2N7002应用到上面电路中呢又起着什么作用呢下面我们来分析一下。

如果沿着a、b两条线将电路切断。

那么MCU1的TX引脚被上拉为5VMCU2的RX引脚也被上拉为3.3V。

2N7002的S、D引脚对应图中的2、3引脚截止就相当于a、b两条线将电路切断。

也就是说此电路在2N7002截止的时候是可以做到给两个MCU引脚输送对应的工作电压。

下面进一步分析

RX配置为串口接收引脚此时2N7002的S、D引脚对应图中的2、3引脚截止2N7002里面的二极管3--2方向不通。

那么MCU2

MCU1

TX发送低电平0V此时2N7002的S、D引脚依然截止但是2N7002里面的二极管2--3方向通即VCC2、R2、2N7002里的二极管、MCU1

RX为0V。

该电路从MCU1到MCU2方向数据传输达到了电平转换的效果。

接下来分析

TX发送高电平3.3V此时Vgs图中1、2引脚电压差电压差约等于02N7002截止2N7002里面的二极管3--2方向不通此时MCU1

MCU2

TX发送低电平0V此时Vgs图中1、2引脚电压差电压差约等于3.3V2N7002导通2N7002里面的二极管3--2方向不通VCC1、R1、2N7002里的二极管、MCU2

1、场效应管的源极S、栅极G、漏极D分别对应于三极管的发射极e、基极b、集电极c它们的作用相似图一所示是N沟道MOS管和NPN型晶体三极管引脚图二所示是P沟道MOS管和PNP型晶体三极管引脚对应图。

2、场效应管是电压控制电流器件由VGS控制ID普通的晶体三极管是电流控制电流器件由IB控制IC。

MOS管道放大系数是跨导gm当栅极电压改变一伏时能引起漏极电流变化多少安培。

晶体三极管是电流放大系数贝塔β当基极电流改变一毫安时能引起集电极电流变化多少。

3、场效应管栅极和其它电极是绝缘的不产生电流而三极管工作时基极电流IB决定集电极电流IC。

因此场效应管的输入电阻比三极管的输入电阻高的多。

4、场效应管只有多数载流子参与导电三极管有多数载流子和少数载流子两种载流子参与导电因少数载流子浓度受温度、辐射等因素影响较大所以场效应管比三极管的温度稳定性好。

5、场效应管在源极未与衬底连在一起时源极和漏极可以互换使用且特性变化不大而三极管的集电极与发射极互换使用时其特性差异很大b

6、场效应管的噪声系数很小在低噪声放大电路的输入级及要求信噪比较高的电路中要选用场效应管。

7、场效应管和普通晶体三极管均可组成各种放大电路和开关电路但是场效应管制造工艺简单并且又具有普通晶体三极管不能比拟的优秀特性在各种电路及应用中正逐步的取代普通晶体三极管目前的大规模和超大规模集成电路中已经广泛的采用场效应管。

8、输入阻抗高驱动功率小由于栅源之间是二氧化硅SiO2绝缘层栅源之间的直流电阻基本上就是SiO2绝缘电阻一般达100MΩ左右交流输入阻抗基本上就是输入电容的容抗。

由于输入阻抗高对激励信号不会产生压降有电压就可以驱动所以驱动功率极小灵敏度高。

一般的晶体三极管必需有基极电压Vb再产生基极电流Ib才能驱动集电极电流的产生。

晶体三极管的驱动是需要功率的Vb×Ib。

9、开关速度快:MOSFET的开关速度和输入的容性特性的有很大关系由于输入容性特性的存在使开关的速度变慢但是在作为开关运用时可降低驱动电路内阻加快开关速度输入采用了后述的“灌流电路”驱动加快了容性的充放电的时间。

MOSFET只靠多子导电不存在少子储存效应因而关断过程非常迅速开关时间在10—100ns之间工作频率可达100kHz以上普通的晶体三极管由于少数载流子的存储效应使开关总有滞后现象影响开关速度的提高目前采用MOS管的开关电源其工作频率可以轻易的做到100K/S150K/S,这对于普通的大功率晶体三极管来说是难以想象的。

10、无二次击穿由于普通的功率晶体三极管具有当温度上升就会导致集电极电流上升正的温度电流特性的现象而集电极电流的上升又会导致温度进一步的上升温度进一步的上升更进一步的导致集电极电流的上升这一恶性循环。

而晶体三极管的耐压VCEO随管温度升高是逐步下降这就形成了管温继续上升、耐压继续下降最终导致晶体三极管的击穿这是一种导致电视机开关电源管和行输出管损坏率占95%的破环性的热电击穿现象也称为二次击穿现象。

MOS管具有和普通晶体三极管相反的温度电流特性即当管温度或环境温度上升时沟道电流IDS反而下降。

例如一只IDS10A的MOS

FET开关管当VGS控制电压不变时在250C温度下IDS3A当芯片温度升高为1000C时IDS降低到2A这种因温度上升而导致沟道电流IDS下降的负温度电流特性使之不会产生恶性循环而热击穿。

也就是MOS管没有二次击穿现象可见采用MOS管作为开关管其开关管的损坏率大幅度的降低近两年电视机开关电源采用MOS管代替过去的普通晶体三极管后开关管损坏率大大降低也是一个极好的证明。

11、MOS管导通后其导通特性呈纯阻性普通晶体三极管在饱和导通是几乎是直通有一个极低的压降称为饱和压降既然有一个压降那么也就是普通晶体三极管在饱和导通后等效是一个阻值极小的电阻但是这个等效的电阻是一个非线性的电阻电阻上的电压和流过的电流不能符合欧姆定律而MOS管作为开关管应用在饱和导通后也存在一个阻值极小的电阻但是这个电阻等效一个线性电阻其电阻的阻值和两端的电压降和流过的电流符合欧姆定律的关系电流大压降就大电流小压降就小导通后既然等效是一个线性元件线性元件就可以并联应用当这样两个电阻并联在一起就有一个自动电流平衡的作用所以MOS管在一个管子功率不够的时候可以多管并联应用且不必另外增加平衡措施非线性器件是不能直接并联应用的。

​堆栈是内存中一段连续的存储区域用来保存一些临时数据:​​嵌入式开发中更接近底层的汇编与C语言​​。

堆栈操作由PUSH、POP两条指令来完成。

而程序内存可以分为几个区

栈区stack堆区Heap全局区static文字常亮区程序代码区

程序编译之后全局变量静态变量已经分配好内存空间在函数运行时程序需要为局部变量分配栈空间当中断来时也需要将函数指针入栈保护现场以便于中断处理完之后再回到之前执行的函数。

普通单片机启动时不需要用bootloader将代码从ROM搬移到RAM。

取指令分析指令执行指令

根据PC的值从程序存储器读出指令送到指令寄存器。

然后分析执行执行。

这样单片机就从内部程序存储器去代码指令从RAM存取相关数据。

RAM取数的速度是远高于ROM的但是普通单片机因为本身运行频率不高所以从ROM取指令慢并不影响。

而STM32的CPU运行的频率高远大于从ROM读写的速度。

所以需要用bootloader将代码从ROM搬移到RAM。

使用栈就象我们去饭馆里吃饭只管点菜发出申请、付钱、和吃使用吃饱了就走不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作他的好处是快捷但是自由度小。

使用堆就象是自己动手做喜欢吃的菜肴比较麻烦但是比较符合自己的口味而且自由度大。

其实堆栈就是单片机中的一些存储单元这些存储单元被指定保存一些特殊信息比如地址保护断点和数据保护现场。

这些存储单元中的内容都是程序执行过程中被中断打断时事故现场的一些相关参数。

如果不保存这些参数单片机执行完中断函数后就无法回到主程序继续执行了。

这些存储单元的地址被记在了一个叫做堆栈指针SP的地方。

从上面的描述可以看得出来在代码中是如何占用堆和栈的。

可能很多人还是无法理解这里再结合STM32的开发过程中与堆栈相关的内容来进行讲述。

这里重点知道堆栈数值大小就行。

还有一段AREA区域表示分配一段堆栈数据段。

数值大小可以自己修改也可以使用STM32CubeMX数值大小配置如下图所示。

Stack_Size

1字节的栈空间。

所以在函数内有较多局部变量时就需要注意是否超过我们配置的堆栈大小。

函数参数

这里要强调一点传递指针只占4字节如果传递的是结构体就会占用结构大小空间。

提示在函数嵌套,递归时系统仍会占用栈空间。

Heap_Size

大部分人应该很少使用malloc来分配堆空间。

虽然堆上的数据只要程序员不释放空间就可以一直访问但是如果忘记了释放堆内存那么将会造成内存泄漏甚至致命的潜在错误。

MDK中RAM占用大小分析

经常在线调试的人可能会分析一些底层的内容。

这里结合MDK-ARM来分析一下RAM占用大小的问题。

在MDK编译之后会有一段RAM大小信息

这里461640转换成16进制就是0x668在进行在调试时会出现

这个MSP就是主堆栈指针一般我们复位之后指向的位置复位指向的其实是栈顶

而MSP指向地址0x20000668是0x20000000偏移0x668而得来。

具体哪些地方占用了RAM可以参看map文件中【Image

Symbol

不同的平台的汇编代码是不一样的最早的汇编在50年代就发明了比很多人的父母的年龄都大老掉牙不用学习怎么写汇编。

一个公司有一个人知道怎么写汇编就够了。

但要学习读汇编

为什么学习汇编

直接翻译为机器语言性能最高。

优秀的C语言效率只能达到汇编的80%左右。

其他高级语言跟汇编一比差得更远。

语言越高级性能越差。

很多bootloader和BIOS用汇编写汇编操作的是电脑手机刚刚上电时硬件和初始化的那些命令它们的性能的要求比较高效率高开机速度更快。

分析问题

个人认为编程人与机器对话我们写C写JAVA但是电脑并不认识这些语言电脑只认识0和1所以需要一个人来翻译这些语言这个翻译官就是编译器但是编译器不能百分之百准确的表达程序员的意思也就是所谓的翻译有反义。

例如编译器为了性能好一点可能会优化变量和语句这个过程可能好心办坏事把有用的操作优化了。

因此只有看懂一些汇编语句才能分析程序真正执行的流程。

在问题难以定位的情况下汇编可能是分析问题的最后一根稻草。

帮助理解硬件

有些学校的单片机课程是以汇编进行教学的主要原因就是汇编更贴近硬件。

不过我不赞成这种做法C语言能快速做出一点东西有利于学生在放弃之前增加成就感好坚持下去。

但是汇编确实更贴近硬件。

LDR指令

所指地址处连续的4个字节1个字的数据传送到目的寄存器中。

LDR指令的寻址方式比较灵活,实例如下

LDR

将存储器地址为R1的字数据读入寄存器R0并将R1R2的值存入R1。

LDR

将存储器地址为R1的字数据读入寄存器R0并将R18的值存入R1。

LDR

将存储器地址为R1R2的字数据读入寄存器R0并将R1R2的值存入R1。

LDR

#2]将存储器地址为R1R2*4的字数据读入寄存器R0并将R1R2*4的值存入R1。

LDR

将存储器地址为R1的字数据读入寄存器R0并将R1R2*4的值存入R1。

LDR

Label为程序标号Label必须是当前指令的-4~4KB范围内。

LDR

的指令格式与LDR相似只不过它是将存储器地址中的8位1个字节读到目的寄存器中。

LDRH的指令格式也与LDR相似它是将内存中的16位半字读到目的寄存器中。

LDR

这里的LDR不是arm指令而是伪指令。

这个时候与MOVE很相似只不过MOV指令后的立即数是有限制的。

这个立即数必须是0X00-OXFF范围内的数经过偶数次右移得到的数所以MOV用起来比较麻烦因为有些数不那么容易看出来是否合法。

如何在KEIL下阅读汇编

汇编窗口也指向了对应的语句。

但是在执行C语言的第一行之前仍然有许多操作要做比如变量放在哪在哪里调用了main函数等这些操作都被集成开发环境IDE给封装起来了。

我们必须知道在执行main函数之前有许多事情要做只不过初学的时候不必理会。

以下是C语言源码功能是点亮LED。

//main.c

1000。

这里边有个知识点叫做大小端模式以下简单讲解不能理解就记住。

1000

最终可以看到C语句被翻译成了意料之中的汇编语句自己的意图被机器准确的理解了。

软件定时器是用程序模拟出来的定时器可以由一个硬件定时器模拟出成千上万个软件定时器这样程序在需要使用较多定时器的时候就不会受限于硬件资源的不足这是软件定时器的一个优点即数量不受限制。

但由于软件定时器是通过程序实现的其运行和维护都需要耗费一定的CPU资源同时精度也相对硬件定时器要差一些。

在LinuxuC/OSFreeRTOS等操作系统中都带有软件定时器原理大同小异。

典型的实现方法是通过一个硬件定时器产生固定的时钟节拍每次硬件定时器中断到就对一个全局的时间标记加一每个软件定时器都保存着到期时间。

程序需要定期扫描所有运行中的软件定时器将各个到期时间与全局时钟标记做比较以判断对应软件定时器是否到期到期则执行相应的回调函数并关闭该定时器。

以上是单次定时器的实现若要实现周期定时器即到期后接着重新定时只需要在执行完回调函数后获取当前时间标记的值加上延时时间作为下一次到期时间继续运行软件定时器即可。

3.1

软件定时器需要一个硬件时钟源作为基准这个时钟源有一个固定的节拍(可以理解为秒针的每次滴答)用一个32位的全局变量tickCnt来记录这个节拍的变化

static

一旦开始运行tickCnt将不停地加一而每个软件定时器都记录着一个到期时间只要tickCnt大于该到期时间就代表定时器到期了。

3.2

软件定时器的数据结构决定了其执行的性能和功能一般可分为两种数组结构和链表结构。

什么意思呢这是(多个)软件定时器在内存中的存储方式可以用数组来存也可以用链表来存。

两者的优劣之分就是两种数据结构的特性之分数组方式的定时器查找较快但数量固定无法动态变化数组大了容易浪费内存数组小了又可能不够用适用于定时事件明确且固定的系统链表方式的定时器数量可动态增减易造成内存碎片(如果没有内存管理)查找的时间开销相对数组大适用于通用性强的系统LinuxuC/OSFreeRTOS等操作系统用的都是链表式的软件定时器。

本文使用数组结构

数组和链表是软件定时器整体的数据结构当具体到单个定时器时就涉及软件定时器结构体的定义软件定时器所具有的功能与其结构体定义密切相关以下是本文中软件定时器的结构体定义

typedef

模式有两种到期后就停止的是单次模式到期后重新定时的是周期模式。

typedef

不管哪种模式定时器到期后都将执行回调函数以下是该函数的定义参数指针argv为void指针类型便于传入不同类型的参数。

typedef

上述结构体中的模式state和回调函数指针cb是可选的功能如果系统不需要周期执行的定时器或者不需要到期后自动执行某个函数可删除此二者定义。

3.3

首先是软件定时器的初始化对每个定时器结构体的成员赋初值虽说static变量的初值为0但个人觉得还是有必要保持初始化变量的习惯避免出现一些奇奇怪怪的BUG。

void

SOFT_TIMER_STOPPED;timer[i].mode

0;timer[i].period

启动一个软件定时器不仅要改变其状态为运行状态同时还要告诉定时器什么时候到期(当前tickCnt值加上延时时间即为到期时间)单次定时还是周期定时到期后执行哪个函数函数的参数是什么交代好这些就可以开跑了。

void

SOFT_TIMER_RUNNING;timer[id].mode

mode;timer[id].cb

上面函数中的assert_param()用于参数检查类似于库函数assert()。

3.3.3

本文中软件定时器有三种状态停止运行和超时不同的状态做不同的事情。

停止状态最简单啥事都不做运行状态需要不停地检查有没有到期到期就执行回调函数并进入超时状态超时状态判断定时器的模式如果是周期模式就更新到期时间继续运行如果是单次模式就停止定时器。

这些操作都由一个更新函数来实现

void

SOFT_TIMER_RUNNING:if(timer[i].match

tickCnt_Get())

SOFT_TIMER_TIMEOUT;timer[i].cb(timer[i].argv,

timer[i].argc);

SOFT_TIMER_TIMEOUT:if(timer[i].mode

MODE_ONE_SHOT)

SOFT_TIMER_RUNNING;}break;default:printf(timer[%d]

state

如果定时器跑到一半想把它停掉就需要一个停止函数操作很简单改变目标定时器的状态为停止即可

void

又如果想知道一个定时器是在跑着呢还是已经停下来也很简单返回它的状态

uint8_t

或许这看起来很怪为什么要返回而不是直接读别忘了在前面3.2节中定义的定时器数组是个静态全局变量该变量只能被当前源文件访问当外部文件需要访问它的时候只能通过函数返回这是一种简单的封装保持程序的模块化。

3.4

定时器TMR_STRING_PRINT只执行一次1s后在串口1打印一串字符

定时器TMR_TWINKLING为周期定时器周期为0.5s每次到期都将取反LED0的状态实现LED0的闪烁

定时器TMR_DELAY_ON执行一次3s后点亮LED1跟第一个定时器不同的是此定时器的回调函数是个空函数nop()点亮LED1的操作通过主循环中判断定时器的状态来实现这种方式在某些场合可能会用到。

static

{USART1_Init(115200);TIM4_Init(TIME_BASE_MS);TIM4_NVIC_Config();LED_Init();printf(I

just

spoon.\r\n);softTimer_Start(TMR_STRING_PRINT,

MODE_ONE_SHOT,

5);softTimer_Start(TMR_TWINKLING,

MODE_PERIODIC,

0);softTimer_Start(TMR_DELAY_ON,

MODE_ONE_SHOT,

{softTimer_Update();if(softTimer_GetState(TMR_DELAY_ON)

SOFT_TIMER_TIMEOUT)

STM32启动流程。

如果读者朋友已经有过汇编相关基础能够够好理解本文内容。

汇编语言是比C语言更接近机器底层的编程语言能让我们更好的理解和操纵硬件底层。

STM32三种启动模式

下好程序后重启芯片时SYSCLK的第4个上升沿BOOT引脚的值将被锁存这就是所谓的启动过程。

STM32上电或者复位后代码区始终从0x00000000开始其实就是将存储空间的地址映射到0x00000000中。

三种启动模式如下

从主闪存存储器启动将主Flash地址0x08000000映射到0x00000000这样代码启动之后就相当于从0x08000000开始。

主闪存存储器是STM32内置的Flash作为芯片内置的Flash是正常的工作模式。

一般我们使用JTAG或者SWD模式下载程序时就是下载到这个里面重启后也直接从这启动程序。

从系统存储器启动。

首先控制BOOT0、BOOT1管脚复位后STM32与上述两种方式类似从系统存储器地址0x1FFF

F000开始执行代码。

系统存储器是芯片内部一块特定的区域芯片出厂时在这个区域预置了一段Bootloader就是通常说的ISP程序。

这个区域的内容在芯片出厂后没有人能够修改或擦除即它是一个ROM区。

启动的程序功能由厂家设置。

系统存储器存储的其实就是STM32自带的bootloader代码。

从内置SRAM启动将SRAM地址0x20000000映射到0x00000000,这样代码启动之后就相当于从0x20000000开始。

内置SRAM也就是STM32的内存既然是SRAM自然也就没有程序存储的能力了这个模式一般用于程序调试。

假如我只修改了代码中一个小小的地方然后就需要重新擦除整个Flash比较的费时可以考虑从这个模式启动代码用于快速的程序调试等程序调试完成后在将程序下载到SRAM中。

用户可以通过设置BOOT1和BOOT0引脚的状态来选择在复位后的启动模式。

STM32三种启动模式对应的存储介质均是芯片内置的如下图

串口下载程序原理

从系统存储器启动这种模式启动的程序功能是由厂家设置的。

一般来说这种启动方式用的比较少。

系统存储器是芯片内部一块特定的区域STM32在出厂时由ST在这个区域内部预置了一段BootLoader也就是我们常说的ISP程序这是一块ROM出厂后无法修改。

一般来说我们选用这种启动模式时是为了从串口下载程序因为在厂家提供的BootLoader中提供了串口下载程序的固件可以通过这个BootLoader将程序下载到系统的Flash中。

将BOOT0设置为1BOOT1设置为0然后按下复位键这样才能从系统存储器启动BootLoader在BootLoader的帮助下通过串口下载程序到Flash中程序下载完成后又有需要将BOOT0设置为GND手动复位这样STM32才可以从Flash中启动。

STM32的启动文件与编译器有关不同编译器它的启动文件不同。

虽然启动文件汇编代码各有不同但它们原理类似都属于汇编程序。

拿基于MDK-ARM的启动文件来举例说一下要点内容。

在基于MDK的启动文件开始有一段汇编代码是分配堆栈大小的。

这里重点知道堆栈数值大小就行。

还有一段AREA区域表示分配一段堆栈数据段。

可以使用STM32CubeMX对上面的数值大小进行配置

看下面的汇编代码程序上电之后是跳到Reset_Handler这个位置。

Reset_Handler开始执行再来看如下Reset_Handler汇编代码。

在启动的时候执行了SystemInit这个函数。

执行完SystemInit函数初始化了系统时钟之后跳转到main函数执行。

六、嵌入式代码注入漏洞浅析

随着互联网的发展嵌入式设备正分布在一个充满可以被攻击者利用的源代码级安全漏洞的环境中。

因此嵌入式软件开发人员应该了解不同类型的安全漏洞——特别是代码注入。

术语“代码注入”意味着对程序的常规数据输入可以被制作成“包含代码”并且该程序可以被欺骗来执行该代码。

代码注入缺陷意味着黑客可以劫持现有进程并以与原始进程相同的权限执行任何他们喜欢的代码。

在许多嵌入式系统中进程需要以最高的权限运行因此成功的代码注入攻击可以完全控制机器以及窃取数据导致设备发生故障将其作为其僵尸网络成员或使其永久无法使用。

该程序将数据视为代码并对其进行编译

在大多数情况下程序故意像执行代码一样执行数据是不寻常的但将数据用于构造有意执行的对象却很常见。

格式化字符串漏洞

大多数C程序员熟悉printf函数。

大体上这些格式字符串后跟一个其他参数的列表并且该格式字符串被解释为一组指令用于将剩余的参数呈现为字符串。

大多数用户知道如何编写最常用的格式说明符例如字符串小数和浮点数——sdf——但是不知道还有其他格式字符串指令可以被滥用。

以下是printf函数通常被滥用的一种方式。

有些程序员习惯编译字符串如下

printfstr;

虽然这将在大部分时间内都具有所期望的效果但它是错误的因为printf的第一个参数将被编译为格式字符串。

所以如果str包含任何格式说明符它们就将被这样编译。

例如如果str包含d它会将printf参数列表中的下一个值解释为整数并将其转换为字符串。

在这种情况下没有更多的参数但机器在执行的时候并不了解这一点;

因为在C运行时没有机制可以告诉机器已经没有更多的参数了所以printf将简单地选择恰好在堆栈中的下一个项目将其编译为一个整数并打印出来。

很容易看出这可以用来从栈中打印任意数量的信息。

例如如果str包含dddd则将会打印堆栈上接下来四个字的值。

虽然这是一个代码注入安全漏洞但由于它唯一可能造成的伤害就是可以被用来获取栈中的数据所以它还是可以被原谅的。

可如果位于那里的是敏感数据如密码或证书密钥情况就会变得很糟而且由于攻击者还可以在那里写入任意内存地址因此情况还可能会变得更糟。

使这种糟糕情况的发生成为可能的是格式说明符n。

通常相应的参数是指向整数的指针。

当格式字符串为了建立结果字符串而被编译时一遇到n到目前为止写入的字节数就被放置到由该指针所指示的存储单元中了。

例如在下面的printf完成之后i中的值将为4

printf“1234n”i;

如果函数的实际参数比格式说明符更少那么printf会将任何在堆栈上的数据作为参数编译。

因此如果攻击者可以控制格式字符串那么它们可以将基本上任意的值写入堆栈位置。

因为堆栈是局部变量所在的位置所以它们的值可以被改变。

如果这些变量中有一些是指针那么这个平台甚至可以到达其他非堆栈地址。

真正对攻击者来说有价值的目标是让攻击者控制程序的执行部分。

如果一个局部变量是一个函数指针则攻击者可以通过该指针的后续调用来编写代码实现自己的目标。

当函数返回时攻击者还可以将指令要被送达的地址覆盖重写。

避免代码注入

避免代码注入的最佳方法是通过设计。

如果您可以使用一种永远不会出现漏洞的语言那么这是最好的因为您的代码在构建时就是对一切攻击免疫的。

或者您可以通过设计代码来禁止可能导致这些问题的接口。

不幸的是在嵌入式系统中这些选择并不总是可行的。

即使C是一种危险的语言充斥着漏洞但它仍然是许多组织架构的首选语言。

鉴于此开发人员应该了解其他避免代码注入的方法。

printf“s”str;

这样str的内容只被视为数据。

这是最不费脑子的办法只要你能找到所有应该做出这种修改的地方。

但这对于大型程序来说可能是棘手的特别是对于第三方代码库。

测试漏洞

即使能实现非常高的代码覆盖率的测试也不能触发这些问题。

测试安全漏洞时测试人员必须采取一个攻击者的心态。

诸如模糊测试的技术可能是有用的但是该技术通常太随机无法高度可靠。

静态分析可以有效地发现代码注入漏洞。

注意到早期生成的静态分析工具如lint及其后代衍生产品很不擅长发现这样的漏洞因为想要实现精确的查找漏洞就需要完成整个程序的路径敏感分析。

最近出现的先进的静态分析工具更加有效。

静态分析工具厂商对于哪些接口有危险寻找目标的知识基础以及如何有效地进行这些工作已经积累了丰富的经验。

这里使用的关键技术是污染分析或危险信息流分析。

这些工具通过首先识别潜在风险数据的来源并对信息进行追踪了解信息是如何通过代码不经过验证就流入正在使用的位置的。

代码注入漏洞是危险的安全问题因为它们可能允许攻击者中断程序有时甚至完全控制程序。

那些关心如何在一个充满潜在恶意的互联网环境中确保他们的嵌入式代码能够安全使用的开发人员应该将这样的代码注入漏洞在开发周期和严格的代码检查中尽早消除。

而上面提到的高级静态分析工具是被推荐使用的。

来源http://www.newelectronics.co.uk/electronics-technology/code-injection-a-common-vulnerability/150031/

七、如何优化单片机程序

虽然书写格式并不会影响生成的代码质量但是在实际编写程序时还是应该遵循一定的书写规则一个书写清晰、明了的程序有利于以后的维护。

在书写程序时特别是对于While、for、do…while、if…else、switch…case

1.2

程序中使用的用户标识符除要遵循标识符的命名规则以外一般不要用代数符号(如a、b、x1、y1)作为变量名应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符以增加程序的可读性如count、number1、red、work

1.3

语言是一种高级程序设计语言提供了十分完备的规范化流程控制结构。

因此在采用C

语言设计单片机应用系统程序时首先要注意尽可能采用结构化的程序设计方法这样可使整个应用系统程序结构清晰便于调试和维护。

对于一个较大的应用程序通常将整个程序按功能分成若干个模块不同模块完成不同的功能。

各个模块可以分别编写甚至还可以由不同的程序员编写一般单个模块完成的功能较为简单设计和调试也相对容易一些。

在C

所谓程序模块化不仅是要将整个程序划分成若干个功能模块更重要的是还应该注意保持各个模块之间变量的相对独立性即保持模块的独立性尽量少使用全局变量等。

对于一些常用的功能模块还可以封装为一个应用程序库以便需要时可以直接调用。

但是在使用模块化时如果将模块分成太细太小又会导致程序的执行效率变低(进入和退出一个函数时保护和恢复寄存器占用了一些时间)。

1.4

在程序化设计过程中对于经常使用的一些常数如果将它直接写到程序中去一旦常数的数值发生变化就必须逐个找出程序中所有的常数并逐一进行修改这样必然会降低程序的可维护性。

因此应尽量当采用预处理命令方式来定义常数而且还可以避免输入错误。

1.5

能够使用条件编译(ifdef)的地方就使用条件编译而不使用if

1.6

对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方应当采用圆括号明确指定它们的优先顺序。

一个表达式通常不能写得太复杂如果表达式太复杂时间久了以后自己也不容易看得懂不利于以后的维护。

1.7

对于程序中的函数在使用之前应对函数的类型进行说明对函数类型的说明必须保证它与原来定义的函数类型一致对于没有参数和没有返回值类型的函数应加上“void”说明。

如果需要缩短代码的长度可以将程序中一些公共的程序段定义为函数。

如果需要缩短程序的执行时间在程序调试结束后将部分函数用宏定义来代替。

注意应该在程序调试结束后再定义宏因为大多数编译系统在宏展开之后才会报错这样会增加排错的难度。

1.8

就少一个可以利用的数据存储器空间如果定义了太多的全局变量会导致编译器无足够的内存可以分配而局部变量大多定位于MCU

中使用寄存器操作速度比数据存储器快指令也更多更灵活有利于生成质量更高的代码而且局部变量所能占用的寄存器和数据存储器在不同的模块中可以重复利用。

1.9

许多编译程序有几种不同的优化选项在使用前应理解各优化选项的含义然后选用最合适的一种优化方式。

通常情况下一旦选用最高级优化编译程序会近乎病态地追求代码优化可能会影响程序的正确性导致程序运行出错。

因此应熟悉所使用的编译器应知道哪些参数在优化时会受到影响哪些参数不会受到影响。

2.1

应熟悉算法语言。

将比较慢的顺序查找法用较快的二分查找法或乱序查找法代替插入排序或冒泡排序法用快速排序、合并排序或根排序代替这样可以大大提高程序执行的效率。

选择一种合适的数据结构也很重要比如在一堆随机存放的数据中使用了大量的插入和删除指令比使用链表要快得多。

数组与指针具有十分密切的关系一般来说指针比较灵活简洁而数组则比较直观容易理解。

对于大部分的编译器使用指针比使用数组生成的代码更短执行效率更高。

2.2

能够使用字符型(char)定义的变量就不要使用整型(int)变量来定义能够使用整型变量定义的变量就不要用长整型(long

int)能不使用浮点型(float)变量就不要使用浮点型变量。

当然在定义变量后不要超过变量的作用范围如果超过变量的范围赋值C

2.3

可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。

如下

(1)求余运算

编译器的“%”运算均是调用子程序来完成代码长、执行速度慢。

通常只要求是求2n

(2)平方运算

系列)乘法运算比求平方运算快得多因为浮点数的求平方是通过调用子程序来实现的在自带硬件乘法器的AVR

单片机中如ATMega163

即使是在没有内置硬件乘法器的AVR单片机中乘法运算的子程序比平方运算的子程序代码短执行速度快。

如果是求3

次方如

说明通常如果需要乘以或除以2n都可以用移位的方法代替。

在ICCAVR

中如果乘以2n都可以生成左移的代码而乘以其它的整数或除以任何数均调用乘除法子程序。

用移位的方法得到代码比调用乘除法子程序生成的代码效率高。

实际上只要是乘以或除以一个整数均可以用移位的方法得到结果如

aa*9

(1)循环语对于一些不需要循环变量参加运算的任务可以把它们放到循环外面这里的任务包括表达式、函数的调用、指针运算、数组访问等应该将没有必要执行多次的操作全部集合在一起放到一个init

(2)延时函数

均有为0转移的指令采用后一种方式能够生成这类指令。

在使用while

循环时也一样使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3

但是在循环中有通过循环变量“i”读写数组的指令时使用预减循环时有可能使数组超界要引起注意。

(3)while

在这两种循环中使用do…while循环编译后生成的代码的长度短于while循环。

2.6

在程序中一般不进行非常复杂的运算如浮点数的乘除及开方等以及一些复杂的数学模型的插补运算对这些即消耗时间又消费资源的运算应尽量使用查表的方式并且将数据表置于程序存储区。

如果直接生成所需的表比较困难也尽量在启动时先计算然后在数据存储器中生成所需的表后面在程序运行直接查表就可以了减少了程序执行过程中重复计算的工作量。

2.7

比如使用在线汇编及将字符串和一些常量保存在程序存储器中均有利于优化。

乘除法优化

目前单片机的市场竞争很激烈许多应用出于性价比的考虑选择使用程序存储空间较小如1K2K的小资源8位MCU芯片进行开发。

一般情况下这类MCU没有硬件乘法、除法指令在程序必须使用乘除法运算时如果单纯依靠编译器调用内部函数库来实现常常会有代码量偏大、执行效率偏低的缺点。

上海晟矽微电子推出的MC30、MC32系列MCU采用了RISC架构在小资源8位MCU领域有广大的用户群和广泛的应用本文就以晟矽微电的这两个系列产品的指令集为例结合汇编与C编译平台给大家介绍一种既省时又节约资源的乘除法算法。

3.1

单片机中的乘法是二进制的乘法也就是把乘数的各个位与被乘数相乘然后再相加得出因为乘数和被乘数都是二进制所以实际编程时每一步的乘法可以用移位实现。

例如乘数R301101101被乘数R411000101乘积R1R0。

步骤如下

1、清空乘积R1R0

2、乘数的第0位是1那被乘数R4需要乘上二进制数1也就是左移0位加到R1R0里;

4、乘数的第2位是1那被乘数R4需要乘上二进制数100也就是左移2位加到R1R0里

5、乘数的第3位是1那被乘数R4需要乘上二进制数1000也就是左移3位加到R1R0里

7、乘数的第5位是1那被乘数R4需要乘上二进制数100000也就是左移5位加到R1R0里

8、乘数的第6位是1那被乘数R4需要乘上二进制数1000000也就是左移6位加到R1R0里

R1R0

在实际的程序设计过程中程序优化有两个目标提高程序运行效率和减少代码量。

我们来看下本文提供的汇编算法和普通C语言编程的效率和代码量对比。

下表是程序运行效率的对比数据可能会有小的偏差很明显汇编编译出来的运行时间要比C语言减少很多。

下表是程序代码量的对比数据可能会有小的偏差汇编占用的程序空间也要比C语言小很多。

综上两点本文介绍的乘法算法各方面使用情况都要比C编译好很多。

如果大家在使用过程中原有的程序不能满足应用需求例如遇到程序空间不够或者运行时间太久等问题都可以按照以上方式进行优化。

汇编语言最接近机器语言的。

在汇编语言中可以直接操作寄存器调整指令执行顺序。

由于汇编语言直接面对硬件平台而不同的硬件平台的指令集及指令周期均有较大差异这样会对程序的移植和维护造成一定的不便所以我们针对精简指令集做了乘法运算的例程便于大家的移植和理解。

3.2

单片机中的除法也是二进制的除法和现实中数学的除法类似是从被除数的高位开始按位对除数进行相除取余的运算得出的余数再和之后的被除数一起再进行新的相除取余的运算直到除不尽为止因为单片机中的除法是二进制的每个步骤除出来的商最大只有1所以我们实际编程时可以把每一步的除法看作减法运算。

例如被除数R3R41100110001101101除数R511000101商R1R0余数R2。

步骤如下

1、清空商R1R0余数R2

3、上一步余数并上被除数次高位第14位得1111仍然比除数小商为0余数R2为11

4、直到放开第8位后得11001100比除数大商得1余数R2为111

5、上一步余数并上被除数第7位得1110没有除数大商为0余数R2为1110

6、上一步余数并上被除数第6位得11101没有除数大商为0余数R2为11101

7、按照以上步骤直到放开了被除数得第3位得11101101比除数大商为1余数R2为101000

8、上一步余数并上被除数第2位得1010001没有除数大商为0余数R2为1010001

9、上一步余数并上被除数第1位得10100010没有除数大商为0余数R2为10100010

10、上一步余数并上被除数第0位得101000101比除数大商为1余数R2为10000000

11、然后把以上所有步骤中得商从左至右依次排列就是最后的商100001001余数为最后算得的余数10000000。

R3R4

下表是程序运行效率和代码量的对比数据可能会有小的偏差很明显本文提供的汇编算法要优化的很多。

以下是针对精简指令集做的除法运算16/8位的例程便于大家的移植和理解。

在半导体存储器的发展中静态存储器(SRAM)由于其广泛的应用成为其中不可或缺的重要一员。

随着微电子技术的迅猛发展SRAM逐渐呈现出高集成度、快速及低功耗的发展趋势。

近年来SRAM在改善系统性能、提高芯片可靠性、降低成本等方面都起到了积极的作用。

今天就带你详细了解一下到底什么是SRAM在了解SRAM之前有必要先说明一下RAMRAM主要的作用就是存储代码和数据供CPU在需要的时候调用。

但是这些数据并不是像用袋子盛米那么简单更像是图书馆中用书架摆放书籍一样不但要放进去还要能够在需要的时候准确的调用出来虽然都是书但是每本书是不同的。

对于RAM等存储器来说也是一样的虽然存储的都是代表0和1的代码但是不同的组合就是不同的数据。

让我们重新回到书和书架上来如果有一个书架上有10行和10列格子每行和每列都有0-9的编号有100本书要存放在里面那么我们使用一个行的编号一个列的编号就能确定某一本书的位置。

在RAM存储器中也是利用了相似的原理。

现在让我们回到RAM存储器上对于RAM存储器而言数据总线是用来传入数据或者传出数据的。

因为存储器中的存储空间是如果前面提到的存放图书的书架一样通过一定的规则定义的所以我们可以通过这个规则来把数据存放到存储器上相应的位置而进行这种定位的工作就要依靠地址总线来实现了。

对于CPU来说RAM就像是一条长长的有很多空格的细线每个空格都有一个唯一的地址与之相对应。

如果CPU想要从RAM中调用数据它首先需要给地址总线发送编号请求搜索图书数据然后等待若干个时钟周期之后数据总线就会把数据传输给CPU看图更直观一些

小圆点代表RAM中的存储空间每一个都有一个唯一的地址线同它相连。

当地址解码器接收到地址总线的指令“我要这本书”地址数据之后它会根据这个数据定位CPU想要调用的数据所在位置然后数据总线就会把其中的数据传送到CPU。

“Static

RAM静态随机存储器”的简称所谓“静态”是指这种存储器只要保持通电里面储存的数据就可以恒常保持。

这里与我们常见的DRAM动态随机存储器不同具体来看看有哪些区别

SRAM不需要刷新电路即能保存它内部存储的数据而DRAMDynamic

Random

Memory每隔一段时间要刷新充电一次否则内部的数据即会消失因此SRAM具有较高的性能功耗较小。

Cache它利用晶体管来存储数据与DRAM相比SRAM的速度快但在相同面积中SRAM的容量要比其他类型的内存小。

但是SRAM也有它的缺点集成度较低相同容量的DRAM内存可以设计为较小的体积但是SRAM却需要很大的体积同样面积的硅片可以做出更大容量的DRAM因此SRAM显得更贵。

还有SRAM的速度快但昂贵一般用小容量SRAM作为更高速CPU和较低速DRAM

最后总结一下

SRAM成本比较高DRAM成本较低1个场效应管加一个电容SRAM存取速度比较快DRAM存取速度较慢电容充放电时间SRAM一般用在高速缓存中DRAM一般用在内存条里

九、STM32如何分配原理图IO

的功能说明这个我们可以从官方的数据手册里面找到。

在学习的时候有两个官方资料我们会经常用到一个是参考手册英文叫

Data

数据手册主要用于芯片选型和设计原理图时参考参考手册主要用于在编程的时候查阅。

在数据手册中有关引脚定义的部分在

Pinouts

LQFP100我们在数据手册中找到这个封装的引脚定义然后根据引脚序号一个一个复制出来整理成

excel

对于了解一点汇编编程的人就可以知道堆栈是内存中一段连续的存储区域用来保存一些临时数据。

堆栈操作由PUSH、POP两条指令来完成。

而程序内存可以分为几个区

栈区stack堆区Heap全局区static文字常亮区程序代码区

程序编译之后全局变量静态变量已经分配好内存空间在函数运行时程序需要为局部变量分配栈空间当中断来时也需要将函数指针入栈保护现场以便于中断处理完之后再回到之前执行的函数。

普通单片机启动时不需要用bootloader将代码从ROM搬移到RAM。

但是STM32单片机需要可以参考相关文章STM32代码的启动过程。

这里我们可以先看看单片机程序执行的过程单片机执行分三个步骤

取指令分析指令执行指令

根据PC的值从程序存储器读出指令送到指令寄存器。

然后分析执行执行。

这样单片机就从内部程序存储器去代码指令从RAM存取相关数据。

RAM取数的速度是远高于ROM的但是普通单片机因为本身运行频率不高所以从ROM取指令慢并不影响。

而STM32的CPU运行的频率高远大于从ROM读写的速度。

所以需要用bootloader将代码从ROM搬移到RAM相关文章:单片机中的RAM

使用栈就象我们去饭馆里吃饭只管点菜发出申请、付钱、和吃使用吃饱了就走不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作他的好处是快捷但是自由度小。

使用堆就象是自己动手做喜欢吃的菜肴比较麻烦但是比较符合自己的口味而且自由度大。

其实堆栈就是单片机中的一些存储单元这些存储单元被指定保存一些特殊信息比如地址保护断点和数据保护现场。

这些存储单元中的内容都是程序执行过程中被中断打断时事故现场的一些相关参数。

如果不保存这些参数单片机执行完中断函数后就无法回到主程序继续执行了。

这些存储单元的地址被记在了一个叫做堆栈指针SP的地方。

从上面的描述可以看得出来在代码中是如何占用堆和栈的。

可能很多人还是无法理解这里再结合STM32的开发过程中与堆栈相关的内容来进行讲述。

这里重点知道堆栈数值大小就行。

还有一段AREA区域表示分配一段堆栈数据段。

数值大小可以自己修改也可以使用STM32CubeMX数值大小配置如下图所示。

Stack_Size

1字节的栈空间。

所以在函数内有较多局部变量时就需要注意是否超过我们配置的堆栈大小。

函数参数

这里要强调一点传递指针只占4字节如果传递的是结构体就会占用结构大小空间。

提示在函数嵌套,递归时系统仍会占用栈空间。

Heap_Size

大部分人应该很少使用malloc来分配堆空间。

虽然堆上的数据只要程序员不释放空间就可以一直访问但是如果忘记了释放堆内存那么将会造成内存泄漏甚至致命的潜在错误。

MDK中RAM占用大小分析

经常在线调试的人可能会分析一些底层的内容。

这里结合MDK-ARM来分析一下RAM占用大小的问题。

在MDK编译之后会有一段RAM大小信息

这里461640转换成16进制就是0x668在进行在调试时会出现

这个MSP就是主堆栈指针一般我们复位之后指向的位置复位指向的其实是栈顶

而MSP指向地址0x20000668是0x20000000偏移0x668而得来。

具体哪些地方占用了RAM可以参看map文件中【Image

Symbol



SEO优化服务概述

作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。

百度官方合作伙伴 白帽SEO技术 数据驱动优化 效果长期稳定

SEO优化核心服务

网站技术SEO

  • 网站结构优化 - 提升网站爬虫可访问性
  • 页面速度优化 - 缩短加载时间,提高用户体验
  • 移动端适配 - 确保移动设备友好性
  • HTTPS安全协议 - 提升网站安全性与信任度
  • 结构化数据标记 - 增强搜索结果显示效果

内容优化服务

  • 关键词研究与布局 - 精准定位目标关键词
  • 高质量内容创作 - 原创、专业、有价值的内容
  • Meta标签优化 - 提升点击率和相关性
  • 内容更新策略 - 保持网站内容新鲜度
  • 多媒体内容优化 - 图片、视频SEO优化

外链建设策略

  • 高质量外链获取 - 权威网站链接建设
  • 品牌提及监控 - 追踪品牌在线曝光
  • 行业目录提交 - 提升网站基础权威
  • 社交媒体整合 - 增强内容传播力
  • 链接质量分析 - 避免低质量链接风险

SEO服务方案对比

服务项目 基础套餐 标准套餐 高级定制
关键词优化数量 10-20个核心词 30-50个核心词+长尾词 80-150个全方位覆盖
内容优化 基础页面优化 全站内容优化+每月5篇原创 个性化内容策略+每月15篇原创
技术SEO 基本技术检查 全面技术优化+移动适配 深度技术重构+性能优化
外链建设 每月5-10条 每月20-30条高质量外链 每月50+条多渠道外链
数据报告 月度基础报告 双周详细报告+分析 每周深度报告+策略调整
效果保障 3-6个月见效 2-4个月见效 1-3个月快速见效

SEO优化实施流程

我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:

1

网站诊断分析

全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。

2

关键词策略制定

基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。

3

技术优化实施

解决网站技术问题,优化网站结构,提升页面速度和移动端体验。

4

内容优化建设

创作高质量原创内容,优化现有页面,建立内容更新机制。

5

外链建设推广

获取高质量外部链接,建立品牌在线影响力,提升网站权威度。

6

数据监控调整

持续监控排名、流量和转化数据,根据效果调整优化策略。

SEO优化常见问题

SEO优化一般需要多长时间才能看到效果?
SEO是一个渐进的过程,通常需要3-6个月才能看到明显效果。具体时间取决于网站现状、竞争程度和优化强度。我们的标准套餐一般在2-4个月内开始显现效果,高级定制方案可能在1-3个月内就能看到初步成果。
你们使用白帽SEO技术还是黑帽技术?
我们始终坚持使用白帽SEO技术,遵循搜索引擎的官方指南。我们的优化策略注重长期效果和可持续性,绝不使用任何可能导致网站被惩罚的违规手段。作为百度官方合作伙伴,我们承诺提供安全、合规的SEO服务。
SEO优化后效果能持续多久?
通过我们的白帽SEO策略获得的排名和流量具有长期稳定性。一旦网站达到理想排名,只需适当的维护和更新,效果可以持续数年。我们提供优化后维护服务,确保您的网站长期保持竞争优势。
你们提供SEO优化效果保障吗?
我们提供基于数据的SEO效果承诺。根据服务套餐不同,我们承诺在约定时间内将核心关键词优化到指定排名位置,或实现约定的自然流量增长目标。所有承诺都会在服务合同中明确约定,并提供详细的KPI衡量标准。

SEO优化效果数据

基于我们服务的客户数据统计,平均优化效果如下:

+85%
自然搜索流量提升
+120%
关键词排名数量
+60%
网站转化率提升
3-6月
平均见效周期

行业案例 - 制造业

  • 优化前:日均自然流量120,核心词无排名
  • 优化6个月后:日均自然流量950,15个核心词首页排名
  • 效果提升:流量增长692%,询盘量增加320%

行业案例 - 电商

  • 优化前:月均自然订单50单,转化率1.2%
  • 优化4个月后:月均自然订单210单,转化率2.8%
  • 效果提升:订单增长320%,转化率提升133%

行业案例 - 教育

  • 优化前:月均咨询量35个,主要依赖付费广告
  • 优化5个月后:月均咨询量180个,自然流量占比65%
  • 效果提升:咨询量增长414%,营销成本降低57%

为什么选择我们的SEO服务

专业团队

  • 10年以上SEO经验专家带队
  • 百度、Google认证工程师
  • 内容创作、技术开发、数据分析多领域团队
  • 持续培训保持技术领先

数据驱动

  • 自主研发SEO分析工具
  • 实时排名监控系统
  • 竞争对手深度分析
  • 效果可视化报告

透明合作

  • 清晰的服务内容和价格
  • 定期进展汇报和沟通
  • 效果数据实时可查
  • 灵活的合同条款

我们的SEO服务理念

我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。

提交需求或反馈

Demand feedback