环球关注:为什么MISRA要求你不要使用位域-本文告诉你真相

时间:2023-06-20 12:19:08 来源:嵌入式Lee

本文转自公众号,欢迎关注

为什么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.1CMSIS

core_cmxx.h中定义

CMSIS中进行了定义,寄存器个别使用位域

1./*IOdefinitions(accessrestrictionstoperipheralregisters)*/2./**3.defgroupCMSIS_glob_defsCMSISGlobalDefines4.5.IOTypeQualifiersareused6.litospecifytheaccesstoperipheralvariables.7.liforautomaticgenerationofperipheralregisterdebuginformation.8.*/9.#ifdef__cplusplus10.#define__Ivolatile/*!
4.2ST
1./**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测试可以保证按照固定指定大小访问,但是不保证其他编译器也支持该选项,最好能不使用就不使用位域。

审核编辑黄宇

关键词:
x 广告
x 广告

Copyright ©  2015-2022 北冰洋家电网版权所有  备案号:沪ICP备2020036824号-3   联系邮箱:562 66 29@qq.com