1. 项目概述:嵌入式系统中的条件控制与高效数据获取
在嵌入式开发,尤其是基于MC68341这类经典微控制器的项目中,我们常常面临两个核心挑战:如何让程序根据瞬息万变的系统状态(如传感器读数、按键事件、通信标志)做出快速、准确的分支决策;以及如何在有限的存储空间和计算能力下,高效地处理复杂的非线性数据转换,比如将ADC原始值转换为物理量,或者实现一个非线性的控制曲线。这两个挑战的答案,就藏在处理器的指令集深处——条件测试与查表插值(TBL)指令。
条件测试是程序控制流的基石。它不像高级语言中的if-else那样直观,而是直接与处理器的条件码寄存器(CCR)的各个状态位(如进位C、零Z、负N、溢出V)挂钩。通过一条简单的条件分支指令(如BCC、BNE),程序就能基于一次算术或逻辑运算的结果,决定是顺序执行还是跳转,这是实现任何复杂逻辑判断的基础。而TBL指令,则是将“空间换时间”这一思想发挥到极致的利器。它允许我们在内存中预先存储一个函数或映射关系的离散采样点(即“表”),当输入一个介于这些采样点之间的值时,TBL指令能自动进行线性插值计算,快速返回一个近似结果。这对于实现三角函数、对数、温度补偿曲线等计算密集型任务,在缺乏硬件浮点单元的MCU上,是提升性能的关键手段。
本文将深入拆解MC68341手册中关于这两部分的核心内容。我不会止步于翻译手册,而是结合我多年在8/16位MCU上“摸爬滚打”的经验,带你理解为什么要这样设计,如何在项目中实际应用它们,以及在实际编程中会遇到哪些“坑”和应对技巧。无论你是正在维护一个古老的68341项目,还是想深入理解嵌入式底层编程的精髓,这篇文章都将提供从原理到实战的完整视角。
2. 条件测试:程序决策的微观逻辑
2.1 条件码寄存器(CCR)与状态位解析
在深入条件测试之前,我们必须先理解它的裁判——条件码寄存器。在MC68341(以及大多数68000家族处理器)中,CCR是状态寄存器(SR)的低8位。它就像程序运行时的“仪表盘”,实时反映上一次算术或逻辑操作的结果。几个关键位决定了程序流的走向:
- C(进位位):指示无符号数运算的最高位发生了进位(加法)或借位(减法)。这对于处理多精度算术(比如64位加法用32位寄存器实现)至关重要。
- Z(零位):当操作结果的所有位都为0时置位。这是判断两个数是否相等最直接的标志。
- N(负位/符号位):设置为与结果的最高位(符号位)相同。用于判断有符号数的正负。
- V(溢出位):指示有符号数运算的结果超出了寄存器能表示的范围。例如,两个正数相加得到了负数(或两个负数相加得到了正数),V位就会被置位。
注意:很多初学者会混淆C(进位)和V(溢出)。简单来说,C关心的是无符号数的“位溢出”,而V关心的是有符号数的“值溢出”。例如,
0xFF + 0x01在8位运算中,结果0x00会产生进位(C=1),但对有符号数(-1 + 1 = 0)而言并未溢出(V=0)。
每一次ADD、SUB、CMP、MOVE(到CCR)、AND、OR等指令执行后,这些标志位都会根据结果被更新。后续的条件分支指令(如BCC、BEQ)正是通过检查这些位的组合状态来决定是否跳转。
2.2 条件测试助记符与逻辑表达式详解
MC68341手册中的表5-12是理解条件测试的钥匙。它不仅仅是一个列表,更是一套完整的逻辑判断体系。我们将其拆解并加入实际编程语义:
| 助记符 | 条件 | 编码 | 逻辑测试 (基于CCR) | 编程语义(常用于...) |
|---|---|---|---|---|
| T | True | 0000 | 1 | 无条件执行(总是跳转) |
| F | False | 0001 | 0 | 从不执行(占位或特殊用途) |
| HI | High | 0010 | C • Z | 无符号数大于 (C=0且Z=0) |
| LS | Low or Same | 0011 | C + Z | 无符号数小于或等于 (C=1或Z=1) |
| CC | Carry Clear | 0100 | C | 无符号数比较中“大于或等于”(加法无进位/减法无借位) |
| CS | Carry Set | 0101 | C | 无符号数比较中“小于”(加法有进位/减法有借位) |
| NE | Not Equal | 0110 | Z | 结果不等于零(两数不相等) |
| EQ | Equal | 0111 | Z | 结果等于零(两数相等) |
| VC | Overflow Clr | 1000 | V | 有符号数运算未溢出 |
| VS | Overflow Set | 1001 | V | 有符号数运算溢出 |
| PL | Plus | 1010 | N | 有符号数结果为正或零 (N=0) |
| MI | Minus | 1011 | N | 有符号数结果为负 (N=1) |
| GE | Greater or Eq | 1100 | N • V + N • V | 有符号数大于或等于 (N和V相同) |
| LT | Less Than | 1101 | N • V + N • V | 有符号数小于 (N和V不同) |
| GT | Greater Than | 1110 | N • V • Z + N • V • Z | 有符号数大于 (Z=0且N和V相同) |
| LE | Less or Equal | 1111 | Z + N • V + N • V | 有符号数小于或等于 (Z=1或N和V不同) |
核心逻辑解析:
- HI/LS vs GT/LE:这是最容易出错的地方。
HI和LS用于无符号数比较,而GT和LE用于有符号数比较。在C语言中,if (a > b),如果a和b是unsigned int,编译后很可能对应BHI;如果是int,则对应BGT。用错会导致比较结果完全错误。 - GE/LT的逻辑:
N • V + N • V这个表达式等价于“N异或V的结果为0”。为什么?因为(N•V)表示N和V都为1,(N•V)表示N和V都为0,这两种情况都满足N==V。当有符号数运算(A-B)的结果N和V相同时,说明结果可靠地反映了大小关系,且结果非负(N=0)或为负(N=1但未溢出),因此A >= B。 - GT/LE的Z位作用:
GT在GE的基础上增加了Z=0的条件,即排除了相等的情况。LE则是LT或EQ的并集。
2.3 条件指令的实战应用与陷阱规避
理解了条件码,我们来看如何用它们控制程序。除了最常用的Bcc(条件分支),还有DBcc(条件递减循环)、Scc(条件置位字节)和TRAPcc(条件陷阱)。
1. 高效循环与边界检查:DBcc指令是编写紧凑循环的神器。它先进行条件测试,如果条件为假(cc不满足),则对指定的数据寄存器进行减1操作,若结果不为-1则进行相对跳转。
MOVE.W #100-1, D0 ; 循环计数器初始化为99(循环100次) LOOP_START: ; ... 循环体代码 ... DBF D0, LOOP_START ; 当D0减到-1时跳出循环这里DBF是DBRA的别名,条件为F(False),意味着总是先执行减1和跳转,直到D0变为-1。这比用SUBQ和Bcc两条指令实现更高效。
2. 条件置位的妙用:Scc指令可以根据条件测试结果,将目标操作数的整个字节设置为全1($FF)或全0($00)。这在实现布尔标志或小型查找表时非常有用。
CMP.B #100, D1 ; 比较D1与100 SGT D2 ; 如果D1>100(有符号),则D2=$FF,否则D2=$00 ; 现在D2可以作为一个布尔值使用3. 实战避坑指南:
- 指令执行不影响CCR:
MOVE指令(除非目标为CCR/SR)、LEA、PEA、JMP、JSR等指令不影响CCR。如果你在CMP之后使用了这些指令,再执行Bcc,分支判断的依据可能已经不是你以为的那个比较结果了。务必确保条件分支紧跟在影响CCR的指令之后。 TRAPcc的独特之处:手册提到F(False)条件对Bcc不可用,但TRAPcc可用。TRAPF意味着“永不陷阱”,看似无用,但在某些调试或特定协议场景下,可以作为预留的指令槽或用于动态修改代码(将TRAPF在内存中改为TRAPxx来动态激活陷阱# 1. 概述
本文,我们来分享 MyBatis 的日志模块,对应logging包。如下图所示:
在 《精尽 MyBatis 源码解析 —— 项目结构一览》 中,简单介绍了这个模块如下:
无论在开发测试环境中,还是在线上生产环境中,日志在整个系统中的地位都是非常重要的。良好的日志功能可以帮助开发人员和测试人员快速定位 Bug 代码,也可以帮助运维人员快速定位性能瓶颈等问题。目前的 Java 世界中存在很多优秀的日志框架,例如 Log4j、 Log4j2、Slf4j 等。
MyBatis 作为一个设计优良的框架,除了提供详细的日志输出信息,还要能够集成多种日志框架,其日志模块的一个主要功能就是集成第三方日志框架。
本文涉及的类如下图所示:
下面,我们逐小节来分享。
2. LogFactory
org.apache.ibatis.logging.LogFactory,Log 工厂类。
2.1 构造方法
// LogFactory.java /** * Marker to be used by logging implementations that support markers */ public static final String MARKER = "MYBATIS"; /** * 使用的 Log 的构造方法 */ private static Constructor<? extends Log> logConstructor; static { // <1> 逐个尝试,判断使用哪个 Log 的实现类,即初始化 logConstructor 属性 tryImplementation(LogFactory::useSlf4jLogging); tryImplementation(LogFactory::useCommonsLogging); tryImplementation(LogFactory::useLog4J2Logging); tryImplementation(LogFactory::useLog4JLogging); tryImplementation(LogFactory::useJdkLogging); tryImplementation(LogFactory::useNoLogging); }logConstructor静态属性,使用的 Log 的构造方法。在<1>处,基于尝试的方式,初始化logConstructor属性。而尝试的顺序,就是使用 Log 的实现类的优先级。也就是说,如果多个 Log 的实现类同时存在的情况下,按照 Slf4J > Commons Logging > Log4J2 > Log4J > Jdk Logging > No Logging的顺序,选择对应的 Log 实现类。
#tryImplementation(Runnable runnable)方法,尝试初始化logConstructor属性。代码如下:
// LogFactory.java private static void tryImplementation(Runnable runnable) { if (logConstructor == null) { try { runnable.run(); } catch (Throwable t) { // ignore } } }如果
logConstructor为空,则执行runnable进行初始化。如果
logConstructor非空,则忽略。
#useSlf4jLogging()方法,尝试使用 Slf4J 。代码如下:
// LogFactory.java public static synchronized void useSlf4jLogging() { setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class); }在该方法内部,会调用
#setImplementation(Class<? extends Log> implClass)方法,尝试初始化logConstructor。代码如下:// LogFactory.java private static void setImplementation(Class<? extends Log> implClass) { try { // 获得参数为 String 的构造方法 Constructor<? extends Log> candidate = implClass.getConstructor(String.class); // 创建 Log 对象 Log log = candidate.newInstance(LogFactory.class.getName()); if (log.isDebugEnabled()) { log.debug("Logging initialized using '" + implClass + "' adapter."); } // 创建成功,意味着可以使用,设置为 logConstructor logConstructor = candidate; } catch (Throwable t) { throw new LogException("Error setting Log implementation. Cause: " + t, t); } }- 通过反射的方式,创建
implClass对应的 Log 对象。如果创建成功,则意味着implClass存在,可以使用。否则,会抛出异常。
- 通过反射的方式,创建
其它#useCommonsLogging()、#useLog4J2Logging()、#useLog4JLogging()、#useJdkLogging()、#useNoLogging()方法,代码类似,就不重复解析了。
2.2 getLog
#getLog(...)方法,获得 Log 对象。代码如下:
// LogFactory.java public static Log getLog(Class<?> aClass) { return getLog(aClass.getName()); } public static Log getLog(String logger) { try { return logConstructor.newInstance(logger); } catch (Throwable t) { throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t); } }3. Log
org.apache.ibatis.logging.Log,MyBatis Log 接口。代码如下:
// Log.java public interface Log { boolean isDebugEnabled(); boolean isTraceEnabled(); void error(String s, Throwable e); void error(String s); void debug(String s); void trace(String s); void warn(String s); }- 和主流的 Log 框架的接口基本一致。
3.1 Log4jImpl
org.apache.ibatis.logging.log4j.Log4jImpl,实现 Log 接口,Log4j 实现类。代码如下:
// Log4jImpl.java public class Log4jImpl implements Log { /** * Log 对象 */ private final Log log; public Log4jImpl(String clazz) { // 获得 Log 对象 log = Logger.getLogger(clazz); } @Override public boolean isDebugEnabled() { return log.isDebugEnabled(); } @Override public boolean isTraceEnabled() { return log.isTraceEnabled(); } @Override public void error(String s, Throwable e) { log.error(s, e); } @Override public void error(String s) { log.error(s); } @Override public void debug(String s) { log.debug(s); } @Override public void trace(String s) { log.trace(s); } @Override public void warn(String s) { log.warn(s); } }- 在构造方法中,
log属性,通过org.apache.log4j.Logger#getLogger(String name)方法,获得 Log 对象。所以,最终使用的 Log 实现类,是 Log4j 提供的。
其它实现类,和 Log4jImpl 的思路一致,所以本文就不重复解析了。感兴趣的胖友,可以自己看看。
4. 代理
在logging包中,我们可以看到jdbc包,基于 JDBC 调试,打印执行的 SQL 等等。这块内容,我们在后续的文章中,详细解析。而jdbc包中的类,会调用logging包中的类,打印日志。但是,实际上,logging包中的类,使用的是代理模式,打印的日志是交给commons-logging、log4j2等等日志框架。
可能这么说,有点抽象,我们直接看一个例子。BaseJdbcLogger 类:
// BaseJdbcLogger.java public abstract class BaseJdbcLogger { /** * Log 对象 */ protected Log log; // ... 省略其它属性 public BaseJdbcLogger(Log log) { this.log = log; } }在 BaseJdbcLogger 中,有个
log属性。而它的实现类,例如 ConnectionLogger :// ConnectionLogger.java public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { /** * Connection 对象 */ private final Connection connection; private ConnectionLogger(Connection conn, Log statementLog) { super(statementLog); // 设置 log 属性 this.connection = conn; } @Override public Object invoke(Object proxy, Method method, Object[] params) throws Throwable { // ... 省略代码 } }在 ConnectionLogger 中,虽然
log属性,但是通过构造方法传入的statementLog参数,进行设置。那么,这个
statementLog参数,是怎么创建的呢?在org.apache.ibatis.logging.jdbc.ConnectionLogger#newInstance方法,代码如下:// ConnectionLogger.java public static Connection newInstance(Connection conn, Log statementLog) { InvocationHandler handler = new ConnectionLogger(conn, statementLog); ClassLoader cl = Connection.class.getClassLoader(); return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler); }通过
LogFactory#getLog方法,获得 Log 对象。代码如下:// LogFactory.java public static Log getLog(Class<?> aClass) { return getLog(aClass.getName()); } public static Log getLog(String logger) { try { return logConstructor.newInstance(logger); } catch (Throwable t) { throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t); } }- 这样,
log属性,最终会是logConstructor创建的 Log 对象。
- 这样,
😈 可能胖友会问,为什么要这么绕呢?直接使用 Log 对应的实现类不就好了么?从目前来看,确实是这样。不过,在commons-logging包中,提供了org.apache.commons.logging.impl.Jdk14Logger类,可以适配java.util.logging。所以,通过这样的方式,可以更好的适配多种日志框架。
5. 适配器
在logging包中,还有commons-logging、log4j2、slf4j等等包,里面都是适配器。例如说,commons-logging包下的JakartaCommonsLoggingImpl类,代码如下:
// JakartaCommonsLoggingImpl.java public class JakartaCommonsLoggingImpl implements Log { private final Log log; public JakartaCommonsLoggingImpl(String clazz) { // 获得 commons-logging Log 对象 log = LogFactory.getLog(clazz); } @Override public boolean isDebugEnabled() { return log.isDebugEnabled(); } @Override public boolean isTraceEnabled() { return log.isTraceEnabled(); } @Override public void error(String s, Throwable e) { log.error(s, e); } @Override public void error(String s) { log.error(s); } @Override public void debug(String s) { log.debug(s); } @Override public void trace(String s) { log.trace(s); } @Override public void warn(String s) { log.warn(s); } }- 在构造方法中,
log属性,通过org.apache.commons.logging.LogFactory#getLog(String name)方法,获得commons-logging的 Log 对象。所以,最终使用的 Log 实现类,是commons-logging提供的。
其它适配器,和 JakartaCommonsLoggingImpl 的思路一致,所以本文就不重复解析了。感兴趣的胖友,可以自己看看。
6. 总结
总的来说,logging包,整体结构如下:
FROM 《Mybatis3.3.x技术内幕(四):五鼠闹东京之执行器Executor设计原本》
- 最上层是 Log 接口。
- 接着是 Log 接口的适配器,例如 Log4jImpl、Slf4jImpl 等等。
- 接着是 Log 接口的代理,例如 ConnectionLogger、PreparedStatementLogger 等等。在
jdbc包下,我们会详细解析。 - 最后是 LogFactory 工厂,负责创建对应的 Log 对象。