zoukankan      html  css  js  c++  java
  • 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文件中, 在开发时可以多看一眼, 避免不必要的时间浪费.

  • 相关阅读:
    .net core读取appsettings.config中文乱码问题
    vs2017错误:当前页面的脚本发生错误
    VS Code中无法识别npm命令
    Visual Studio报错/plugin.vs.js,行:1074,错误:缺少标识符、字符串或数字
    记录一次在生成数据库服务器上出现The timeout period elapsed prior to completion of the operation or the server is not responding.和Exception has been thrown by the target of an invocation的解决办法
    Java集合框架
    java hash表
    Java Dictionary 类存储键值
    java数据结构 栈stack
    java封装
  • 原文地址:https://www.cnblogs.com/milton/p/15786712.html
Copyright © 2011-2022 走看看