由于STM8单片机本身内存比较小,而系统自带的printf()函数又比较占据空间,所以在稍微大一点的工程中有时候一使用 printf() 函数就会导致单片机内存不足,于是想着能不能自己写一个比较小的函数来实现类似printf()函数的功能。经过网上查找资料和总结终于找到了一个占用内存比较小,又能实现串口打印功能的方法。
现在将自己的方法分享出来,这里使用 STM8S003F3P6单片机测试。
首先新建一个工程,专门用来测试串口功能。
串口部分相关代码如下:
//串口
void Uart1_IO_Init( void )
{
PD_DDR |= ( 1 << 5 ); //输出模式 TXD
PD_CR1 |= ( 1 << 5 ); //推挽输出
PD_DDR &= ~( 1 << 6 ); //输入模式 RXD
PD_CR1 &= ~( 1 << 6 ); //浮空输入
}
//波特率最大可以设置为38400
void Uart1_Init( unsigned int baudrate )
{
unsigned int baud;
baud = 16000000 / baudrate;
Uart1_IO_Init();
UART1_CR1 = 0; //禁止发送和接收
UART1_CR2 = 0; //8 bit
UART1_CR3 = 0; //1 stop
UART1_BRR2 = ( unsigned char )( ( baud & 0xf000 ) >> 8 ) | ( ( unsigned char )( baud & 0x000f ) );
UART1_BRR1 = ( ( unsigned char )( ( baud & 0x0ff0 ) >> 4 ) );
UART1_CR2_bit.REN = 1; //接收使能
UART1_CR2_bit.TEN = 1; //发送使能
UART1_CR2_bit.RIEN = 1; //接收中断使能
}
//阻塞式发送函数
void SendChar( unsigned char dat )
{
while( ( UART1_SR & 0x80 ) == 0x00 ); //发送数据寄存器空
UART1_DR = dat;
}
//发送字符串
void SendString( unsigned char* s )
{
while( 0 != *s )
{
SendChar( *s );
s++;
}
}
//接收中断函数 中断号18
#pragma vector = 20 // IAR中的中断号,要在STVD中的中断号上加2
__interrupt void UART1_Handle( void )
{
unsigned char res = 0;
UART1_SR &= ~( 1 << 5 ); //RXNE 清零
res = UART1_DR;
}
主程序代码如下:
void main( void )
{
__asm( "sim" ); //禁止中断
SysClkInit();
delay_init( 16 );
LED_GPIO_Init();
Uart1_IO_Init();
Uart1_Init( 9600 );
__asm( "rim" ); //开启中断
while( 1 )
{
}
}
编译代码之后,在map文件中查看代码大小。
可以看到此时串口占用代码大小只有100个字节左右,占用代码量很小,接下来在主程序中使用printf()函数打印数据。
在main()函数中使用printf()函数打印字符串,这里要注意一点,要使用printf()函数时,需要在串口中实现 putchar( ) 函数,因为printf( )函数最终会调用这个putchar( ) 函数打印字符。putchar( ) 函数和SendChar( )函数的功能其实是一样的。
int putchar( int ch )
{
while( !( UART1_SR & 0X80 ) ); //循环发送,直到发送完毕
UART1_DR = ( u8 ) ch;
return ch;
}
通过串口助手可以看到字符串可以正常打印了,此时再次查看map文件。
串口文件的字节数变成了110多个,比原来多了10个,但是总的字节数由六百多个变成了两千多个,翻了好几倍。这里只使用了一条打印语句,内存占用多了2K多。
下面不使用系统自带的这个printf( )函数了,重新自己实现一个printf( ) 函数。在网上找到一个使用最多的方法。
#include "stdarg.h"
#include "stdio.h"
#include "string.h"
//自定义串口 的printf 函数
char uart_buf[50];
void uart_printf( char* fmt, ... ) //无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表
{
u16 i, j;
va_list ap; //va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
va_start( ap, fmt ); //va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束
vsprintf( ( char* )uart_buf, fmt, ap ); // 把生成的格式化的字符串存放在这里
va_end( ap );
i = strlen( ( const char* )uart_buf ); //此次发送数据的长度
for( j = 0; j < i; j++ ) //循环发送数据
{
while( ( UART1_SR & 0x80 ) == 0x00 ); //发送数据寄存器空
UART1_DR = uart_buf[j];
}
}
使用可变参数的原理,通过调用系统 vsprintf() 函数来打印。
通过调用自定义的 uart_printf()函数来打印字符串,通过串口助手可以看到,字符串也能正常打印。下面通过map文件查看文件占用内存大小。
通过map文件中的内容可以看到,这个方法比直接调用系统的printf()函数占用的空间还大。可以网上的这个方法也不好。于是又重新找了一个方法。
#include "stdarg.h"
#include "stdio.h"
/* 功能:将int型数据转为2,8,10,16进制字符串 参数:value --- 输入的int整型数 str --- 存储转换的字符串 radix --- 进制类型选择 注意:8位单片机int字节只占2个字节 */
char *int2char( int value, char *str, unsigned int radix )
{
char list[] = "0123456789ABCDEF";
unsigned int tmp_value;
int i = 0, j, k = 0;
if ( NULL == str )
{
return NULL;
}
if ( 2 != radix && 8 != radix && 10 != radix && 16 != radix )
{
return NULL;
}
if ( radix == 10 && value < 0 )
{
//十进制且为负数
tmp_value = ( unsigned int )( 0 - value );
str[i++] = '-';
k = 1;
}
else
{
tmp_value = ( unsigned int )value;
}
//数据转换为字符串,逆序存储
do
{
str[i ++] = list[tmp_value % radix];
tmp_value /= radix;
}
while( tmp_value );
str[i] = '\0';
//将逆序字符串转换为正序
char tmp;
for ( j = k; j < ( i + k ) / 2; j++ )
{
tmp = str[j];
str[j] = str[i - j - 1 + k];
str[i - j - 1 + k] = tmp;
}
return str;
}
/* 功能:将double型数据转为字符串 参数:value --- 输入的double浮点数 str --- 存储转换的字符串 eps --- 保留小数位选择,至少保留一个小数位,至多保留4个小数位 注意:8位单片机int字节只占2个字节 */
void flaot2char( double value, char *str, unsigned int eps )
{
unsigned int integer;
double decimal;
char list[] = "0123456789";
int i = 0, j, k = 0;
//将整数及小数部分提取出来
if ( value < 0 )
{
decimal = ( double )( ( ( int )value ) - value );
integer = ( unsigned int )( 0 - value );
str[i ++] = '-';
k = 1;
}
else
{
integer = ( unsigned int )( value );
decimal = ( double )( value - integer );
}
//整数部分数据转换为字符串,逆序存储
do
{
str[i ++] = list[integer % 10];
integer /= 10;
}
while( integer );
str[i] = '\0';
//将逆序字符串转换为正序
char tmp;
for ( j = k; j < ( i + k ) / 2; j++ )
{
tmp = str[j];
str[j] = str[i - j - 1 + k];
str[i - j - 1 + k] = tmp;
}
//处理小数部分
if ( eps < 1 || eps > 4 )
{
eps = 4;
}
//精度问题,防止输入1.2输出1.19等情况
double pp = 0.1;
for ( j = 0; j <= eps; j++ )
{
pp *= 0.1;
}
decimal += pp;
while ( eps )
{
decimal *= 10;
eps --;
}
int tmp_decimal = ( int )decimal;
str[i ++] = '.';
k = i;
//整数部分数据转换为字符串,逆序存储
do
{
str[i ++] = list[tmp_decimal % 10];
tmp_decimal /= 10;
}
while( tmp_decimal );
str[i] = '\0';
//将逆序字符串转换为正序
for ( j = k; j < ( i + k ) / 2; j++ )
{
tmp = str[j];
str[j] = str[i - j - 1 + k];
str[i - j - 1 + k] = tmp;
}
str[i] = '\0';
}
void uart_printf( char * Data, ... )
{
const char *s;
int d;
char buf[16];
unsigned char txdata;
va_list ap;
va_start( ap, Data );
while ( * Data != 0 )
{
if ( * Data == 0x5c )
{
switch ( *++Data )
{
case 'r':
txdata = 0x0d;
SendChar( txdata );
Data ++;
break;
case 'n':
txdata = 0x0a;
SendChar( txdata );
Data ++;
break;
default:
Data ++;
break;
}
}
else if ( * Data == '%' )
{
switch ( *++Data )
{
case 's':
s = va_arg( ap, const char * );
for ( ; *s; s++ )
{
SendChar( *( ( unsigned char * )s ) );
}
Data++;
break;
case 'd':
d = va_arg( ap, int );
int2char( d, buf, 10 );
for ( s = buf; *s; s++ )
{
SendChar( *( ( unsigned char * )s ) );
}
Data++;
break;
case 'x':
{
d = va_arg( ap, int );
int2char( d, buf, 16 );
for ( s = buf; *s; s++ )
{
SendChar( *( ( unsigned char * )s ) );
}
Data++;
break;
}
case 'f':
{
double num = va_arg(ap, double);
flaot2char(num, buf, 4);
for (s = buf; *s; s++)
{
SendChar(*((unsigned char *)s));
}
Data++;
break;
}
default:
Data++;
break;
}
}
else
{
SendChar( *( ( unsigned char * )Data ) );
Data++;
}
}
}
这个方法的实现原理是分别将整形数据和浮点型数据转换为字符,然后再通过串口将字符打印出来。转换整形和字符型数据时,分别使用了两个函数来实现。
下面测试这个方法
串口数据可以正常输出。接下来在map文件中查看占用内存大小。
可以看到这个方法总的占空空间也很大,其中串口文件的占用大小也多了1K多。那么能不能优化一下串口中的代码。
通过对上面的代码分析可以看出,串口打印函数主要调用了两个函数,一个而是整形数据转字符串,一个是浮点型数据转字符串,而在平时调试中使用浮点数比较少,那么就可以将浮点数相关的代码屏蔽掉。
在 uart_printf( )函数中将浮点打印相关代码屏掉,这样只需要使用一个整形转字符串的函数就可以了。
继续测试打印功能,可以看到串口打印功能正常,此时查看map文件中占用空间大小。
在map文件中可以看到串口的代码占用空间缩小了一半,代码总的占用空间1K多,是不使用打印功能的2倍,相当于这次自定义的 printf()函数占用空间大概 600多个字节。
通过map文件中可以看出,主要是uart.c这个文件占用的空间变大了,那么想要减小代码的空间占用,只需要优化uart.c这个文件中的代码就行了。通过对代码分析,可以优化部分基本就是switch 语句的分支了,由于自己在调试代码的时候大多数使用的只是整数打印功能,也就是基本只使用到 “%d”,其他的很少用,所以可以将switch 语句其他的分支都注释掉。优化后的代码为:
void uart_printf( char * Data, ... )
{
const char *s;
int d;
char buf[16];
unsigned char txdata;
va_list ap;
va_start( ap, Data );
while ( * Data != 0 )
{
if ( * Data == 0x5c )
{
switch ( *++Data )
{
case 'r':
txdata = 0x0d;
SendChar( txdata );
Data ++;
break;
case 'n':
txdata = 0x0a;
SendChar( txdata );
Data ++;
break;
default:
Data ++;
break;
}
}
else if ( * Data == '%' )
{
switch ( *++Data )
{
/* case 's': s = va_arg( ap, const char * ); for ( ; *s; s++ ) { SendChar( *( ( unsigned char * )s ) ); } Data++; break; */
case 'd':
d = va_arg( ap, int );
int2char( d, buf, 10 );
for ( s = buf; *s; s++ )
{
SendChar( *( ( unsigned char * )s ) );
}
Data++;
break;
/* case 'x': { d = va_arg( ap, int ); int2char( d, buf, 16 ); for ( s = buf; *s; s++ ) { SendChar( *( ( unsigned char * )s ) ); } Data++; break; } case 'f': { double num = va_arg(ap, double); flaot2char(num, buf, 4); for (s = buf; *s; s++) { SendChar(*((unsigned char *)s)); } Data++; break; } */
default:
Data++;
break;
}
}
else
{
SendChar( *( ( unsigned char * )Data ) );
Data++;
}
}
}
优化代码之后,测试一下字符串打印和整形打印的功能。
通过串口助手可以看到,字符串和整数打印功能都正常,下面看一下map文件中占用空间大小。
在map文件中可以看到,注释掉switch 语句的几个分支之后,代码少了一百多个字节。现在总代码量是1K多。可见这个代码基本已经达到最优了,没有多大的优化空间了。但是和前面的方法相比这个方法占用代码量小多了。这个方法相对来说还是比较好的。
在以后工程中就可以使用这个函数进行串口打印了,对于资源比较少的单片机来说,每一片内存都是寸土寸金,能少占用就尽量少占用。
代码工程下载地址: https://download.csdn.net/download/qq_20222919/54824408
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://hxydj.blog.csdn.net/article/details/121697756
内容来源于网络,如有侵权,请联系作者删除!