STC8H开发(四): FwLib_STC8 封装库的介绍和注意事项
目录
- STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解)
- STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解)
- STC8H开发(三): 基于FwLib_STC8的模数转换ADC介绍和演示用例说明
- STC8H开发(四): FwLib_STC8 封装库的介绍和使用注意事项
- STC8H开发(五): SPI驱动nRF24L01无线模块
- STC8H开发(六): SPI驱动ADXL345三轴加速度检测模块
- STC8H开发(七): I2C驱动MPU6050三轴加速度+三轴角速度检测模块
- STC8H开发(八): NRF24L01无线传输音频(对讲机原型)
- STC8H开发(九): STC8H8K64U模拟USB HID外设
- STC8H开发(十): SPI驱动Nokia5110 LCD(PCD8544)
- STC8H开发(十一): GPIO单线驱动多个DS18B20数字温度计
- STC8H开发(十二): I2C驱动AT24C08,AT24C32系列EEPROM存储
前面介绍了如何在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文件中, 在开发时可以多看一眼, 避免不必要的时间浪费.