由于新冠疫情的影响,全球供应链受到了极大的影响,众多芯片价格更是一涨再涨,以出货量最大,应用面最广的stm32f103系列为例,价格由正常价格的7元左右一片涨到了80多元一片,价格整整涨了十多倍。更严重的问题是,你很可能面临着,即使有钱也拿不到货的境地,供货周期长达三个月,这无论是在bom成本上还是交付上都是无法接受的。正是由于这样的困难,国产的芯片迎来了波替换潮,对于一些要求不高的嵌入式应用场合,stc系列51单片机,由于其极低的价格、稳定的货源,成为不错的选择之一。
stm32f103c8与stc8a8k64d4对比主要资源对比
项 | STM32F103V8 | STC8A8K64D4 |
GPIO | 37pin | 43pin |
主频 | 72M | 45M |
flash | 64K | 64K |
RAM | 20K | 8K |
UART | 3 | 4 |
ADC | 10 ch,10位 | 15ch, 12位 |
定时器 | 1tick + 4个32位 | 4个 |
乘除法指令 | 单周期硬件乘除法 | 无 |
价格 | 64元 | 4.5元 |
最重要的是: STC有现货
针对stc8a8k64d4的硬件资源,由于其没有硬件乘除法,导致其在需要进行大量数据计算的应用场景上效率低下,对于一般的应用场景,stc在硬件资源上完全够用,在现有的供应链条件下,STC是一个非常不错的选择。
俗话说有利就有弊,STC带来了不少的硬件成本优势,但是其在编程上却带来了一些不适应,我们在代码编写时,需要特别注意。
由于stc系列单片机是基于51架构,虽然它在内核上进行了加强,但是由于时代的局限性,51架构和现代的MCU架构相比,存在不少的差距。由于现代编译器不断的演进,我们大部人都是使用C语言进行编程,这在一定程度上,帮助我们屏蔽了架构层面的巨大差异。而且由于架构的落后,再加宏晶公司与ST公司本身在IP上面的技术差距,这导致我们在使用STC51的外设时,也有很大的差异。下图是51的架构和stm32的架构,可以看出明显的差距。
编程模型的区别 -- 函数重入
对于C语言的编程模型(请参看我前面的文章《C语言代码组成 - BSS、Data、Stack、Heap、Code、Const》),51和ARM在处理stack上,有巨大的区别。51加架构中,sp指针指向的区域是pdata区域,这个区域最大情况只有256个字节,作为栈来说,是不可能用来分配局部变量的。因此keil编译器对于临时变量,采用了一种叫作变量覆盖的技术,即多个函数使用同一区域作作为临时变量区,下一个函数调用运行时,会将上一个函数运行的临时变量覆盖掉。这样就解决了栈空间不足的问题。而且因为没有进出栈的操作,也提高了代码效率。但这带来的最大的问题就是函数重入问题,即51代码全部是不可重入的,如果一个函数在中断中被调用,那就意味着这个函数必须是可重入的,才能正常运行,而且这个中断函数所调用的下级函数也必须是可重入的。如何解决重入问题:
以上三种方法,各有缺点,方法一简单粗爆,需要消耗更多的资源;方法二会导致中断响应不及时;方法三导致中断响应不及时。所以用户需要根据项目特点,使用合适的策略;
函数传参
C51通过寄存器传递参数,最多只能传递三个参数
乘除法
由于51没有硬件乘除法,而且是8位机,对于除法指令,十分耗时,这会导致程序响应慢。对于除法指令,尽量使用移位操作来代替。
调试问题
STC51是没有像arm那样的jtag调试接口,下载程序和调试是通过串口进行的,但是在使用串口进行单步调试时,非常不稳定,经常通讯不上,导致调试失败。在实际项目中,我是使用uart打印log来进行调试。由于打印的字符串是定义在常量区,过多的打印会导致代码量增大,所以要合理的使用打印。
串口驱动优化
由于STC51的串口只有发送完成中断和接受完成中断,而没有接受空闲中断,这就需要我们使用特殊的处理方式,具体方法可以参见我前面的文章《串口接收中的帧同步》。代码如下
//收发数据的数据结构typedef struct{ u8 id; //串口号 u8 TX_read; //发送读指针 u8 TX_write; //发送写指针 u8 B_TX_busy; //忙标志 u8 RX_Cnt; //接收字节计数 u8 RX_TimeOut; //接收超时 u8 B_RX_OK; //接收块完成} COMx_Define; u8 xdata TX1_Buffer[COM_TX1_Lenth]; //发送缓冲u8 xdata RX1_Buffer[COM_RX1_Lenth]; //接收缓冲//定时器中断//用来判断帧超时, 在1ms中断中调用void Serial_HookMs(void){ if(COM1.RX_TimeOut > 0){ COM1.RX_TimeOut --; } else { //if(COM1.RX_Cnt > 0){ //COM1.B_RX_OK = 1; //} }}//中断函数,发送中断和接受中断void UART1_ISR_Handler (void) interrupt UART1_VECTOR{ if(RI){ RI = 0; if(COM1.RX_Cnt >= COM_RX1_Lenth) COM1.RX_Cnt = 0; RX1_Buffer[COM1.RX_Cnt++] = SBUF; COM1.RX_TimeOut = TimeOutSet1; } if(TI){ TI = 0; if(COM1.TX_read != COM1.TX_write){ SBUF = TX1_Buffer[COM1.TX_read]; if(++COM1.TX_read >= COM_TX1_Lenth) COM1.TX_read = 0; } else COM1.B_TX_busy = 0; }}int Serial_Read(UART_Port_t port, uint8_t* pmsg, int msg_size){ COMx_Define *com = NULL; int len = 0; uint8_t *rbuff = NULL; switch(port){ case UART1: com = &COM1; rbuff = RX1_Buffer; break; default: return -1; } if((com->RX_TimeOut == 0) && (com->RX_Cnt > 0)){ if(msg_size < com->RX_Cnt){ return -2; } len = com->RX_Cnt; memcpy(pmsg, rbuff, len); com->RX_Cnt = 0; return len; } return -3;}//采用中断发送int Serial_Write(UART_Port_t port, uint8_t* pmsg, int len){ COMx_Define *com = NULL; uint8_t *sbuff = NULL; int i; int s_buff_max = 0; void (* uart_send)(u8 dat); switch(port){ case UART1: com = &COM1; sbuff = TX1_Buffer; uart_send = TX1_write2buff; s_buff_max = COM_TX1_Lenth; break; default: return -1; } if(com->B_TX_busy != 0){ //发送中 return -2; } //剩余空间不够 if((s_buff_max - (com->TX_write + s_buff_max - com->TX_read) % s_buff_max) < len){ return -3; } //将数据考贝到缓冲区中; for(i = 1; i < len; i ++){ sbuff[com->TX_write] = pmsg[i]; com->TX_write ++; com->TX_write %= s_buff_max; } uart_send(pmsg[0]); return len;}
使用串口的收发中断以避免阻塞式的收发,用定时器实现帧间隔判断来判断是否是完整一帧,这样极大的增加了程序的效率。
IIC驱动库的坑
在项目中,使用STC8A的IIC操作外设时,新焊接的板子,会出现不断复位重启的现象,通过加打印定位程序是卡死在iic操作中,通过分析程序,在iic的驱动中存在等应答的操作,这是一个死循环,如下:
void Wait(void){ while (!(I2CMSST & 0x40)); I2CMSST &= ~0x40;}
由于新焊接的板子,存在虚焊的问题,导到IIC应答失败,这就使IIC永远 收不到应答,从而死在wait中,看门狗无法喂狗,导致复位。将驱动改为如下:
void Wait(void){ u32 i = 0; while (!(I2CMSST & 0x40)){ i ++; if(i > 250){ break; } } I2CMSST &= ~0x40;}
通过增加超时机制,来避免永远无法应答的问题。
ADC的坑
在项目中,使用ADC采集变送器的电压信号,当ADC采集到的值大于某个值时,输出开关信号,实际中,ADC采集是在主循环中做的,主循环约为1ms,当突然接入电压信号时,差不多要3~4ms,才会输出开关信号,存在很大的时延。按理来说,时延应该在一个循环周期以内,即1ms以内才是正常的。而通过通讯口读取出来的稳定值是正确的。最后通过打印ADC转换值,才发现ADC的值是有一个较缓的上升过程,这是由于ADC的输入电阻和采样电容较大导致的,具体的请参看前面的文章《AD转换中的采样电容和输入电阻》。即使通过调整参数,增加采样时间,依然无法解决此问题,最后解决的办法是连续多次转换(多次测试后为4次),取最后一次的结果作为效值,从而解决此问题。
数据类型
由于C51是8位单片机,其数据类型长度与ARM相比还是很大区别的,比如int在keil c51中,默认是 16位的。如果在调用外部第三方库时,稍不注意,很容易出现数据溢出。具体的可以参看stc8a库中的type_def.h文件。
由于STC51的外设功能相对简单,而且STC的示例完成度很高,使用起来还是很方便,从32位机转到51,在前期会有一个磨合的过程,由于编译器的错误提示功能的强大,再加上网上资源的丰富,一般的问题,基本都能很快解决。总的来说,对于一些适合的应用场景,STC的51单片机是个不错的选择。