在STM8单片机中自己实现 printf()函数功能

x33g5p2x  于2021-12-05 转载在 其他  
字(6.7k)|赞(0)|评价(0)|浏览(754)

由于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

相关文章