STC8H开发(四): FwLib_STC8 封装库的介绍和注意事项
目录
前面已经介绍了如何在Keil5和PlatformIO环境下使用FwLib_STC8, 展示了ADC数模转换的例子. 这篇整体介绍一下这个封装库, 以及使用这个封装库进行开发的注意事项.
动机
写这个封装库的初衷是希望知识和经验能复用, 避免每次开发时都要去查手册. 如果直接用寄存器开发, 在不同的STC型号之间切换就会感受到这个问题, 都得去查手册, 不仅繁琐还容易出错, 费时费力. 如果把一些先验知识代码化, 就能简化这个过程.
在 STC89/STC90 这一代, 几十个SFR还是可以记忆的. 到了STC11, STC12, 开始出现ADC, SPI这些外设, 也还可以接受. 到STC15之后, SFR数量一下子上来, 单单PWM就有十几个SFR, 单凭记忆就很难记住这些东西了. 并且在STC15之后, 同系列之间差异增加, 包括运行时钟都不是固定的几种, 从6MHz到40MHz连续可调, 给基础的定时器和串口设置都带来不少麻烦.
各种工具方式的对比
STC-ISP工具
STC-ISP是宏晶提供的百宝箱型工具, 除了用于烧录外, 还提供了各个型号的延迟函数, 串口初始化函数, 以及代码的用例下载. 软件本身还是很好用的, 主要的问题在于
- 不支持Linux
- 覆盖的功能有限, 仅有延迟, 定时器和串口通信代码生成
- 二次修改可能会影响其它配置, 依然需要查手册
封装库: 逻辑代码化
典型的项目是 HML_FwLib 系列: HML_FwLib_STC12, HML_FwLib_STC11, HML_FwLib_STC10, HML_FwLib_STC89.
这些都是将逻辑代码化, 将实现细节用C语言的方式写在函数里. 就像STM32的SPL和HAL库一样, 用户只需要与库函数打交道, 不需要知道底下的寄存器.
但是这种形式在MCS51这个场景是比较尴尬的: 片内资源太少了.
如果把各种初始化和计算的工作都放到函数里, 就会占用运行资源, 导致固件体积增大, 运行时耗费的内存增加, 一些稍微复杂一点的逻辑就没法跑了. 就像在HML_FwLib_STC12中使用串口后, 初始化之后固件就会占用差不多2K字节, 运行中添加其它逻辑后不小心就会超出内存限制. 以至于后来将串口1初始化单独写了个直接写寄存器的方法.
HML_FwLib 这个系列的封装还存在一个问题, 就是SFR变量名与STC官方的命名不一致. 如果仅仅是在Linux下开发自己的项目, 这个问题不是很重要, 但是如果要将代码迁移到Keil C51, 或者使用其它项目的代码, 就不方便了. 网络上的大部分STC代码是在Keil C51下开发的, 不能直接用 HML_FwLib_STC12 运行, 因为有很多命名要改, 否则编译都无法通过.
使用第三方工具生成代码
这个方式和STC-ISP其实是一样的, 对于STC8, 最初从这个方向做了尝试, 就是 stcmx 这个项目. 这是一个python写的小工具, 在命令行中以交互的形式对各个外设进行选项设置, 然后直接生成C代码.
生成的代码非常简洁, 都是对寄存器的直接赋值, 一步到位直接完成初始化. 风格是这样的
void clock_init()
{
// [ BAH,0,0x00]: 外设端口切换控制寄存器2,串口2/3/4,I2C,比较器
P_SW2 = 0x80;
// [FE01H,1,0x00]: 时钟分频寄存器,ISP可能写入预设值
CLKDIV = 0x00;
// [ 9FH,0,0x00]: IRC频率调整寄存器, ISP可能写入预设值, 0x75:24MHz
IRTRIM = 0x75;
// [ 9EH,0,0x00]: IRC频率微调寄存器, ISP可能写入预设值
LIRTRIM = 0x00;
// [ BAH,0,0x00]: 外设端口切换控制寄存器2,串口2/3/4,I2C,比较器
P_SW2 = 0x00;
}
void timer_init()
{
// [ D6H,0,0x00]: 定时器2高字节
T2H = 0xFF;
// [ D7H,0,0x00]: 定时器2低字节
T2L = 0xCB;
// [ 87H,0,0x30]: 电源控制寄存器
PCON = 0xB0;
// [ 8EH,0,0x01]: 辅助寄存器
AUXR = 0x15;
}
void uart_init()
{
// [ 98H,0,0x00]: 串口1控制寄存器
SCON = 0x50;
// [ 87H,0,0x30]: 电源控制寄存器
PCON = 0xB0;
// [ 8EH,0,0x01]: 辅助寄存器
AUXR = 0x15;
}
这种方式基本上和原生的寄存器开发一样, 不仅节省资源, 也解决了知识复用的问题, 比如我要在36.864MHz下用timer2开启uart1, 波特率为115200, 只需要设置选项, 输入这些数字, 直接就能得到寄存器的初始化代码. 并且也是跨平台的, 在Linux下也可以使用.
但是这种形式的缺点
- 工具本身的开发成本比较高, 等于要在python里面把MCU的每个寄存器每个bit的逻辑都结构化, 并配上文字说明(包含多国化)
- 对持续开发不友好, 在已经生成代码之后, 如果需要对某些项做调整, 那么要么重新生成一遍, 要么继续查手册
在写了一段时间后, 投入太大, 逐渐放弃了这个方向.
封装库: 以宏语句的方式将逻辑代码化
这就是 FwLib_STC8 这个项目的尝试. 在这个封装库中, 90%的寄存器操作都是用宏语句实现的.
我从来都不喜欢宏语句, 但是在这个场景, 确实宏语句有独特的好处. 宏语句提供了一种类似于文字注释的功能
- 在开发阶段, 能提供可读的选项信息, 像VSCode这样的IDE, 会代码提示并且自动补全. 体验类似于方法调用.
- 在编译阶段, 没有用到的宏语句就像注释一样被过滤掉了, 不会出现在编译结果里, 不占任何资源.
- 在运行阶段, 避免了方法调用的堆栈消耗, 宏语句会被翻译成直接的寄存器操作, 消除了方法调用.
唯一比直接使用寄存器赋值更占用资源的地方, 是对寄存器的赋值操作不是一步到位的, 可能会根据配置项的不同被拆成好几步, 但是这点overheat是值得的, 因为这样才能实现不查手册直接用封装库写代码, 调用的每一步知道自己在做什么.
现在的代码就变成了这样的风格
SYS_SetClock();
// UART1, baud 115200, baud source Timer2, 1T mode, interrupt on
UART1_Config8bitUart(UART1_BaudSource_Timer2, HAL_State_ON, 115200);
UART1_SetRxState(HAL_State_ON);
// Enable UART1 interrupt
EXTI_Global_SetIntState(HAL_State_ON);
EXTI_UART1_SetIntState(HAL_State_ON);
使用 FwLib_STC8 开发的注意事项
通过前面的两篇介绍, 能大致了解这个封装库在两个主流开发环境里的使用.
使用Keil C51的用户应该会相对简单, 因为直接将封装库加入项目就可以, 另外频率可以直接用STC-ISP设置, 省掉了维护一套频率参数的烦恼. 而在Linux下的用户, 就需要维护一套编译参数, 用于在程序中指定MCU频率, 如果使用PlatformIO开发, 封装库已经通过library.json做了适配, 只要放入项目lib目录, 就会自动识别并添加到include路径.
在demo目录下有丰富的演示示例, 基本上覆盖了全部片内外设. 另外还有对常见元件, 例如喜闻乐见的MAX7219 8x8点阵, NRF24L01无线模块, SSD1306 OLED屏, ST7735 LCD这些设备的驱动. 翻阅一下演示代码, 能基本了解这个封装库的调用方法.
下面说使用这个封装库开发时需要注意的几点
1. 避免在传参里使用表达式
类似于i++
, var--
这样的都是表达式.
这是宏调用的固有缺陷, 因为宏毕竟不是函数, 它只是字符串模板, 在使用++
, --
这类操作符时, 会将这个操作放到模板里展开, 如果在模板里对这个变量引用了两次, 那么它就会执行两次, 这会造成意想不到的问题.
2. 如果要同时对Keil C51和SDCC兼容, 就必须使用封装库提供的宏
封装库中引入了一些宏定义, 用于保证对 Keil C51 和 SDCC 的兼容性. 命名和形式来源于 sdcc/device/include/mcs51/compiler.h.
如果你希望代码在 Keil C51 和 SDCC 下都能编译, 在编码时就应当使用这些宏, 而不是编译器对应的关键词.
以下是相关的宏定义列表
Macro | Keil C51 | SDCC |
---|---|---|
__BIT | bit | __bit |
__IDATA | idata | __idata |
__PDATA | pdata | __pdata |
__XDATA | xdata | __xdata |
__CODE | code | __code |
SBIT(name, addr, bit) | sbit name = addr^bit | __sbit __at(addr+bit) name |
SFR(name, addr) | sfr name = addr | __sfr __at(addr) name |
SFRX(addr) | (*(unsigned char volatile xdata *)(addr)) | (*(unsigned char volatile __xdata *)(addr)) |
SFR16X(addr) | (*(unsigned int volatile xdata *)(addr)) | (*(unsigned int volatile __xdata *)(addr)) |
INTERRUPT(name, vector) | void name (void) interrupt vector | void name (void) __interrupt (vector) |
INTERRUPT_USING(name, vector, regnum) | void name (void) interrupt vector using regnum | void name (void) __interrupt (vector) __using (regnum) |
NOP() | _nop_() | __asm NOP __endasm |
这些宏定义可以在 include/fw_reg_base.h 中查看
3. 部分宏语句的参数是枚举, 调用时要留意
使用宏语句的一个缺点就是没有类型提示, 虽然在变量名上我已经尽量体现出这个参数的类型, 但是写代码时, IDE是没有提示的. 所以这里需要注意的是, 有一些输入参数是枚举, 在调用时最好切换到声明这个宏的.h文件中看一眼, 这些枚举一般都定义在.h文件的开始部分.
4. 不同MCU之间的资源差异
封装库本身只区分了STC8G和STC8H两个大类, 例如STC8G 有 PCA但是没有PWM, STC8H 中有PWM没有PCA. 在大类的内部, 例如 STC8H 的各个子系列, 在功能上也是有差异的, 例如 STC8H1K 系列的ADC是 10bit, STC8H3K, STC8H8K 的ADC是12bit, 还有通道的数量以及和IO口的映射关系都有区别.
这些区别基本上都列在了对应外设的.h文件中, 在开发时可以多看一眼, 避免不必要的时间浪费.
结束
以上就是对 FwLib_STC8 封装库的开发说明. 希望这个封装库对开发STC8G,STC8H项目有帮助. 如果有问题请留言或在代码仓库中提issue.