STC8H开发(四): FwLib_STC8 封装库的介绍和注意事项

目录

前面介绍了如何在Keil5和PlatformIO环境下使用FwLib_STC8, 还有一个ADC数模转换的例子. 接下来整体介绍一下这个封装库, 以及使用这个封装库进行开发的注意事项. 这篇可能会不断的更新.

关于写 FwLib_STC8 的动机

写这个封装库的初衷, 首先是避免每次在做STC8G和STC8H的开发时去查手册, 这个是最主要的动机; 其次, 是要接近直接使用寄存器开发的效率, 不能因为引入封装库造成很大的资源开销.

在 STC89/STC90 这一代, 几十个SFR还是可以记忆的. 到了STC11, STC12, 开始出现ADC, SPI这些外设, 也还可以接受. 到STC15之后, SFR数量一下子上来, 单单PWM就有十几个SFR, 单凭记忆就很难记住这些东西了. 并且在STC15之后, 同系列之间差异增加, 每个MCU的运行时钟都可能不一样, 从6MHz到40MHz可以自由设定, 就连基础的定时器和串口设置都带来了很大的难度.

STC-ISP工具中提供了一些代码模板, 但是这些代码并非完全可用, 灵活性也不够, 例如延时方法都是不带参数的.

早期的尝试

如果经常在不同的MCU之间切换, 就会感觉到每次写都像是第一次写, 都得去查手册去计算, 还容易出错, 费时费力. 把一些先验知识代码化, 就能简化这个过程, 用一次的时间节省将来无数时间.

逻辑代码化

在MCS51这个场景是比较尴尬的: 片内资源太少了.

如果你把各种初始化和计算的工作都放到代码里, 那么就会占用运行资源, 导致固件体积增大, 运行时耗费的内存增加, 一些稍微复杂一点的逻辑就没法跑了. 就像在 HML_FwLib_STC12 这个项目里的尝试一样, 很好用, 但是也很占资源, 一不小心就超出内存限制. 以至于后来将串口1初始化单独写了个直接写寄存器的方法.

HML_FwLib_STC12 这个项目还存在一个问题, 就是SFR变量名与STC官方的命名不一致. 如果仅仅是在Linux下开发, 自成一体, 这个问题不是很重要, 但是如果要使用网络上其他人的代码, 这些代码大都是在Keil C51下开发的, 就不能使用 HML_FwLib_STC12 快速运行, 因为有很多命名需要改.

使用python工具生成代码

所以对于STC8, 最初从另一个方向做了尝试, 就是 stcmx 这个项目.

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, 只需要设置选项, 输入这些数字, 直接就能得到寄存器的初始化代码.

但是这种形式的缺点是工具本身的开发极其繁琐, 等于要在python里面把MCU的每个寄存器每个bit的逻辑都结构化了, 还得配上文字说明, 可以认为和STM32CubeMx做的事情是类似的.

还有一个更大的缺点是不灵活, 在已经生成代码之后, 如果需要对某些项做调整, 那么要么重新生成一遍, 要么继续查手册.

在写了一段时间后, 投入太大, 逐渐放弃了这个方向.

使用宏的方式将逻辑代码化

这就是 FwLib_STC8 这个项目的尝试, 兼顾了灵活性和节约资源. 我从来都不喜欢宏语句, 但是在这个场景, 确实宏语句有独特的好处.

在 FwLib_STC8 中, 90%的寄存器操作都是用宏语句实现的.

宏语句提供了一种类似于寄存器的文字注释的功能, 在开发时的体验类似于方法调用, 因为像VSCode这样的IDE, 会代码提示并且自动补全.

在编译阶段, 宏语句就会被翻译成直接的寄存器操作, 中间节约了方法调用的堆栈. 没有用到的宏语句不会出现在编译结果里, 不占任何资源. 而如果你写了函数, 函数不管调用没调用, 只要同一个C文件的函数被调用了, 这个C文件里的所有函数都会一并出现在编译结果里. 这样带来的编译结果尺寸差异是很明显的.

唯一比直接使用寄存器赋值更占用资源的地方, 是对SFR的直接赋值操作可能会根据配置项的不同被拆成好几步, 但是这点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);

为什么不使用inline: inline不是强制inline的, 编译器会根据情况判断是否inline, 可能会被作为函数进行调用.

使用 FwLib_STC8 进行开发的方式

使用Keil C51的用户应该会相对简单, 因为直接将封装库加入项目就可以, 另外频率可以直接用STC-ISP设置, 省掉了维护一套频率参数的烦恼. 而在Linux下的用户, 就需要维护一套编译参数, 用于在程序中指定MCU频率, 如果使用PlatformIO开发, 封装库已经通过library.json做了适配, 只要放入项目lib目录, 就会自动识别并添加到include路径.

在demo目录下有丰富的演示示例, 基本上覆盖了全部片内外设. 另外还有对常见元件, 例如喜闻乐见的MAX7219 8x8点阵, NRF24L01无线模块, SSD1306 OLED屏, ST7735 LCD这些设备的驱动.

翻阅一下演示代码, 就能基本了解这个封装库的调用方法.

下面说需要注意的几点

1. 不能随便在参数里使用++, --这类表达式

这是宏调用的固有缺陷, 因为宏毕竟不是函数, 它只是字符串模板, 在使用++, --这类操作符时, 会将这个操作放到模板里展开, 如果在模板里对这个变量引用了两次, 那么它就会执行两次, 这会造成意想不到的问题.

2. 如果要同时对Keil C51和SDCC兼容, 就必须使用封装库提供的宏定义

封装库中引入了一些宏定义, 用于保证对 Keil C51 和 SDCC 的兼容性.

命名和形式来源于 sdcc 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文件中, 在开发时可以多看一眼, 避免不必要的时间浪费.

posted on 2022-01-11 01:20  Milton  阅读(2978)  评论(1编辑  收藏  举报

导航