历史上的今天
今天是:2024年12月27日(星期五)
2021年12月27日 | 在STM8单片机中自己实现 printf()函数功能
2021-12-27 来源:eefocus
由于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] = '




