• 正文
  • 相关推荐
申请入驻 产业图谱

KEIL 的半主机模式是什么?

12小时前
303
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

“使用Keil开发STM32,下载程序后不能运行。在main()入口加打印,啥也没打出来,说明程序都没跑到main()。更奇怪的是,在线调试时要点击三次全速运行才能跑起来!”

这个问题听起来很有戏剧性——点三次才能正常运行,简直像是某种程序世界的魔法咒语。经过一番探究,我们不仅找到了解决方案,还发现了一个嵌入式开发中颇具教育意义的经典陷阱。

问题重现:程序界的“三段式启动”

先来还原一下这个问题的场景:

开发者使用的是常见的ARM开发环境Keil MDK,芯片是STM32等 M3 内核的芯片。程序编译下载一切正常,但一旦运行,板子就像“砖”了一样,毫无反应。

于是通过在main函数入口处添加调试信息,确认程序根本没有执行到主函数。这就像是演员永远无法登上舞台,演出自然无法开始。

最神奇的部分来了:当连接调试器进行在线调试时,第一次点击“全速运行”——程序无反应;第二次点击——还是没反应;第三次点击——程序突然正常跑起来了!

这种“三段式”启动方式,让人不禁联想到老式电视机需要拍打几下才能正常显示的场面。但在这个精确的电子世界里,肯定有更科学的解释。

初步解决方案:两种立竿见影的方法

在排查过程中,我们发现工程中虽然主要使用HAL库进行串口打印,但代码中确实存在printf()函数。于是提出了两种解决方案:

方法一:简单粗暴型

直接把所有printf()函数删除,一了百了。

方法二:微库重定向型

使用MicroLIB+fputc的方式实现串口打印功能:

1、在main.c文件中包含stdio.h

2、重定义fputc函数,将输出重定向到串口

3、在工程Target选项勾选“Use MicroLIB”

这两种方法都能立即解决问题,程序恢复正常,一次运行即可启动。但为什么这样修改就能解决问题?背后的根本原因是什么?

深层探秘:半主机模式——调试器的“隐形手”

要理解这个问题的根源,我们需要了解一个嵌入式开发中的特殊概念——半主机(Semihosting)

半主机是一种让目标板(你的芯片)使用主机(你的电脑)输入输出设备的机制。简单来说,就是允许芯片程序通过调试器,使用PC的显示屏、键盘等外设。

当你调用printf()时,标准C库默认可能会尝试使用半主机模式将输出发送到PC端的调试器窗口,而不是芯片的串口。它会触发一个特殊的中断,调试器捕获这个中断后,在PC端完成显示工作。

问题就出在这里:半主机操作是阻塞的且高度依赖调试器。

如果你的程序在没有适当调试环境的情况下运行,或者调试器没有准备好处理半主机请求,程序就会“卡死”在等待调试器响应的状态。

为什么是“三次”?

这个问题的精妙之处就在于“点三次运行才能成功”的现象,这为我们提供了关键线索。

第一次点击运行:芯片复位,程序开始执行。很快,它进入了C库的初始化代码,并卡在了一个半主机调用上(比如尝试打开stdout)。程序停止响应,就像死机了一样。

第二次点击运行:因为没有复位,程序从刚才停止的位置继续执行。此时,第一次尝试可能已经“唤醒”了调试器对半主机请求的处理能力。这次,半主机调用可能成功完成,程序通过了这个卡点。

第三次点击运行:此时,C运行时库的初始化已经完成,程序不再有致命阻塞点,顺利执行到main函数。

简而言之,通过多次“运行”操作,你无意中让调试器和目标程序之间完成了一次成功的“半主机握手”,侥幸绕过了阻塞点。这是一种典型的时序竞争条件——结果取决于多个事件发生的精确顺序。

嵌入式开发中的常见陷阱

这个“三次启动”问题实际上揭示了嵌入式开发中的几个常见陷阱:

1. 默认配置的陷阱

Keil等IDE在创建新工程时,可能会有默认的库配置,而这些配置不一定适合你的硬件环境。开发者往往专注于自己的业务代码,而忽略了底层库的运行机制。

2. C运行时环境的“隐形”代码

我们通常认为程序是从main函数开始执行的,但实际上,在main之前,还有一段C运行时库的初始化代码。这段“隐形”的代码会设置堆栈、初始化静态变量等,也可能包含标准I/O的初始化。

3. 调试环境与独立运行的差异

程序在调试器下运行与独立运行可能具有完全不同的行为。这种差异可能导致在调试时一切正常,但脱机运行就失败的情况。

更广泛的解决方案和预防措施

除了前面提到的两种方法,还有更多解决类似问题的思路,这几个思路只用于我们了解目标芯片,IDE和宿主机以及 C 语言之间的关系,最好还是使用 MicroLIB。

方法一:使用标准库的重定向(不勾选MicroLIB)

这是最正规的做法,适用于ARM Compiler的标准库。

你需要实现的是半主机模式的重定向,而不是简单的fputc:

// 重定向底层读写函数#include?<stdio.h>#include?<rt_sys.h>
// 重定义__sys_write函数int?_sys_write(int?handle,?const?unsigned?char?*buf,?int?len) {? ??for?(int?i =?0; i < len; i++) {? ? ? ??USART_SendData(USART1, buf[i]);? ? ? ??while?(!(USART1->SR & USART_FLAG_TXE));? ? }? ??return?len;}
// 还需要重定义其他系统调用int?_sys_read(int?handle,?unsigned?char?*buf,?int?len,?int?mode) {? ??return?0;?// 如果不是输入,简单返回}
int?_sys_istty(int?handle) {? ??return?1;}
int?_sys_seek(int?handle,?long?pos) {? ??return?-1;}
int?_sys_close(int?handle) {? ??return?-1;}

?

方法二:直接重定向stdout(更简单的方法)

#include?<stdio.h>
// 直接重定向标准输出到串口void?redirect_stdout_to_uart(void)?{? ??// 在初始化代码中调用此函数? ??setvbuf(stdout,?NULL, _IONBF,?0);}
// 实现__io_putchar函数(ARM标准库会调用这个)int?__io_putchar(int?ch) {? ??USART_SendData(USART1, (uint8_t)ch);? ??while?(!(USART1->SR & USART_FLAG_TXE));? ??return?ch;}

?

方法三:完全自定义printf(最彻底的方法)

如果你不想折腾库函数,可以直接自己实现:

// 自定义的简化版printfvoid?my_printf(const?char?*format, ...)?{? ??char?buffer[128];? ? va_list?args;? ? va_start(args, format);? ??int?len = vsnprintf(buffer,?sizeof(buffer), format,?args);? ? va_end(args);
? ??for?(int?i =?0; i < len; i++) {? ? ? ? USART_SendData(USART1, buffer[i]);? ? ? ??while?(!(USART1->SR & USART_FLAG_TXE));? ? }}

 

结尾

每一位嵌入式开发者都会在职业生涯中遇到各种看似“玄学”的问题。有些问题表现为程序偶尔死机,有些是特定操作顺序才能成功,还有些就像这个“三次启动”问题一样带有某种数学美感。

理解这些问题的根本原因,不仅能够解决当前困境,更能帮助我们建立系统性思维,预防未来可能出现的类似问题。

下次当你遇到嵌入式系统中的“灵异现象”时,不妨停下来思考一下:是不是有什么“隐形”的机制在起作用?调试器、运行时库、硬件外设之间是否存在未被充分理解的交互?

毕竟,在计算机的世界里,从来没有真正的“玄学”,只有尚未理解的科学原理。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录