本文转自公众号,欢迎关注
为什么MISRA要求你不要使用位域-本文告诉你真相 (qq.com)
一.前言做过嵌入式开发的一般会看到一条编程规范:”不要使用位域”,一般都是知其然不其所以然,了解的多一点的可能知道位域是实现相关不具备可移植性,那么继续追问哪些行为是实现相关哪些行为导致移植性问题? 或者还有人知道,存储布局,对齐等行为是实现相关会导致不可移植性。如果再追问位域产生的汇编代码是什么样的,怎么进行读-修改-写操作的?知道这些内容的就更加少之又少了。 读写肯定不能读指定位数,只能字节,或者16位,32位这种,那么编译器到底读写用什么宽度? 这时基本大部分人都不知道了。
【资料图】
知其然知其所以然,尤其是嵌入式开发和硬件结合比较紧密,所以一定要了解细节,我们这一篇从一个问题引出然后去分析查找原因,只有遇到问题然后去分析解决它才会有更深刻的映像。
二.问题分析过程问题是驱动程序中一个寄存器的某个位域修改,导致其他位域的值被修改了。
关键代码如下,
1.typedefunionnfc_ena_union{
2. uint32_tw;
3. struct{
4. /*spienable,oncethespitransiscompleted,thisbitwillbeclearedbyHWautomaticlly*/
5. uint32_tnfc_ena:1; //[0]
6. uint32_treserved_0:3; //[1,3]
7. /*swrequesttousedp*/
8. uint32_tnfc_dp_req:1; //[4]
9. uint32_treserved_1:3; //[5,7]
10. /*duetodelayinreceivingdata,nfcdelayonebeattorx*/
11. uint32_tnfc_rx_delay_en:1; //[8]
12. uint32_treserved_2:7; //[9,15]
13. /*spitransdatalength,unitisbyte,oncethespitransiscompleted,thisbitwillbeclearedbyHWautomaticlly*/
14. uint32_tnfc_data_len:16; //[16,31]
15.}_b;
16.}nfc_ena_u;
1./**
2.*fnintnfc_set_datalen(uint8_tid,uint16_tlen)
3.*param[in]idportid
4.*param[in]lendatalen
5.*retval0ok
6.*retval<0paramerr
7.*
8.*/
9.NFC_INLINEintnfc_set_datalen(uint8_tid,uint16_tlen)
10.{
11. if(id>=HW_NFC_PORT_MAX)
12.{
13. return-1;
14.}
15.nfc_base[id]->nfc_ena._b.nfc_data_len=len;
16. return0;
17.}
执行之前该寄存器值为0x00020100
nfc_base[id]->nfc_ena._b.nfc_data_len= len
汇编代码被优化为了写高16位
执行完后寄存器低16位变为了0
这是因为寄存器硬件上只支持32位的写操作,所以写高16位导致低16位清零了,这是硬件决定的。
二.验证一般想到的就是优化相关,加volatile等,我们分别验证下。
3.1不使能编译器优化编译器优化选项改为”-O0”
代码不变
依然会按照16位访问,导致低16位被清掉。
所以可以看到这个和编译器行为有关,编译器显然不是根据优化等级决定位域的操作宽度,这里而是根据位域的宽度刚好是16位对齐,所以优化为了16位操作指令。
3.2使用volatile避免编译器优化#ifndef__IOM
#define__IOM volatile
#endif
所有uint32_t替换为__IOM uint32_t
还是一样的
显然汇编代码的访问宽度也不受volatile影响。
3.3为什么指定了uint32_t和volatile还会优化。问题来了为什么告诉了编译器是uint32_t和volatile,为什么其还要一意孤行,要优化为16位访问指令呢,答案就是因为是标准没有规定,这是编译器实现行为决定的,所以编译器设计者决定的(当然也会有一些现实考虑的),可能不同编译器行为不同,这里以GCC为例。
GCC编译器文档中可以找到答案
GCC的文档可以看到如下内容,也给出了最好是不使用位域的原因
另外也介绍了位域哪些行为也是编译器实现相关的,所以嵌入式可移植性考虑不要使用位域
那么有没有办法指定编译按照一定大小访问呢,GCC有编译选项可以控制见下一节。
3.4使用编译器选项-fstrict-volatile-bitfields可以看到改为了sw指令,按照32位进行了操作
四.一些厂家做法如下可见
4.1CMSIScore_cmxx.h中定义
CMSIS中进行了定义,寄存器个别使用位域
1./*IOdefinitions(accessrestrictionstoperipheralregisters)*/2./**3.defgroupCMSIS_glob_defsCMSISGlobalDefines4.5.IOTypeQualifiersareused6.litospecifytheaccesstoperipheralvariables.7.liforautomaticgenerationofperipheralregisterdebuginformation.8.*/9.#ifdef__cplusplus10.#define__Ivolatile/*!
4.2ST1./**2.*@briefUniversalSerialBusFullSpeedDevice3.*/4.5.typedefstruct6.{7.__IOuint16_tEP0R;/*!4.3瑞萨__I,__O__ROM也是core_cmxx.h中定义,大量使用位域
1.#ifndef__IM/*!
五.总结结论就是正如很多嵌入式编程规范所描述的(比如MISRA),一般不建议使用位域,因为涉及到位域的访问,存储等行为都是实现定义的,不具备可移植性。
嵌入式领域寄存器的定义也最好不要使用位域,到寄存器级别以寄存器操作为单位即可,每个寄存器都要使用__IM,__OM,__IOM描述。
如果一定要使用位域可以使用-fstrict-volatile-bitfields选项,使用GCC测试可以保证按照固定指定大小访问,但是不保证其他编译器也支持该选项,最好能不使用就不使用位域。
审核编辑黄宇
关键词:
Copyright © 2015-2022 北冰洋家电网版权所有 备案号:沪ICP备2020036824号-3 联系邮箱:562 66 29@qq.com