英文:
Understanding an embedded C language variable declaration
问题
我试图理解一些嵌入式C代码,其中声明了一个变量。
uint8_t *p=(uint8_t *)&NOCAN_REGS;
NOCAN_REGS是在不同文件中定义的结构(请参见下面的链接)。
我的理解是变量“p”是一个指向无符号8位整数的指针,但等号后的类型转换对我来说是一个谜。
我希望能够逐步解释,或提供一个学习资源链接,帮助我掌握这种语法。
* [链接](https://github.com/fastbike/canzero-driver-firmware/blob/master/driver/nocan.h)
英文:
I'm trying to understand some embedded C code that declares a variable.
uint8_t *p=(uint8_t *)&NOCAN_REGS;
The NOCAN_REGS is a structure defined in a different file (see link below)
My understanding is that the variable "p" is a pointer to an unsigned 8 bit integer, but everything from the typecast after the equals sign is a mystery to me.
I would appreciate a step by step explanation, or a link to a learning resource that can help me master this syntax.
答案1
得分: 7
好的,以下是翻译好的部分:
(uint8_t *)&NOCAN_REGS;
从右往左解释(因为按照这个顺序更容易解释):
NOCAN_REGS;
...这是一个全局结构体对象的名称,正如你提到的。
&
&
符号表示你想要一个指向它后面的东西的指针,所以
&NOCAN_REGS;
...意味着“指向 NOCAN_REGS 结构体的指针”。
(uint8_t *)
这个强制类型转换是为了将表达式的类型从 nocan_registers_t *
强制改为 uint8_t *
。也就是说,你告诉编译器你希望表达式的类型是指向无符号字节的指针,而不是指向 nocan_registers_t
的指针。
通常程序员会在想要将结构体的内存当做原始字节缓冲区处理时进行这种类型转换。这是一件棘手的事情,因为当你抛弃类型信息时,编译器通常会为你处理的问题(比如成员变量的字节顺序,它们与适当边界的对齐,填充字节等)现在变成了程序员需要考虑的事情...但在想要将原始内存转储到磁盘或类似情况下,这可能会很有用。
英文:
Okay, here is everything after the =
sign:
(uint8_t *)&NOCAN_REGS;
Taken from right to left (because it's easier to explain in that order):
NOCAN_REGS;
... this is the name of a global struct-object, as you mentioned.
&
The &
sign indicates you want a pointer-to-whatever-is-after-it, so
&NOCAN_REGS
... means "pointer to the NOCAN_REGS struct".
(uint8_t *)
The cast is here to forcibly change the type of the expression from nocan_registers_t *
to uint8_t *
. That is, you are telling the compiler that you want the expression's type to be pointer-to-unsigned-bytes, rather than a pointer-to-a-nocan_registers_t
.
Typically a programmer would do a cast like this when he wants to treat a struct's memory as if it were a raw byte-buffer. It's a tricky thing to do, since when you throw away the type-information like that, issues that the compiler normally takes care of for you (like the endian-ness of member-variables, their alignment to appropriate boundaries, padding bytes, etc) now become things the programmer has to think about... but it can be useful in cases where you want to e.g. dump the raw memory to disk or similar.
答案2
得分: 1
请注意:您链接的代码存在许多缺陷和业余编写的问题,包括许多错误和不良实践。
由于提到了SPI,似乎代码的目的是使用SPI来控制像MCP2515等旧的外部CAN控制器。请注意,大多数工程师在Cortex M和STM32甚至发明之前就停止使用这些外部CAN控制器了。
以下是我在快速浏览代码时发现的问题和代码的功能解释:
-
NOCAN_REGS
显然是一个结构体,对应于通过SPI访问的外部CAN控制器寄存器的内存映射。这个结构体有两个奇怪之处:未启用打包(packing)和一个成员带有volatile
修饰符。如果这是一个片上的CAN控制器,那么整个结构体都应该是
volatile
的。在这种情况下,volatile
仅在与ISR共享变量时防止编译器优化时才起作用。所以它是正确的,但可能需要更好地记录。更糟糕的是,未启用打包,这意味着uint32_t
成员可能被分配到不对齐的地址。因此,将它们作为uint32_t
访问可能会导致硬件错误。这些应该是uint8_t [4]
数组。(或者,填充不应该被禁用,结构体应该按成员方式进行序列化/反序列化。) -
NOCAN_REGS
被声明为全局变量并暴露给整个程序,这是非常不好的实践。它应该被声明为static
,而且不应该在头文件中。如果程序设计合理,除了驱动程序之外,程序的任何其他部分都不应该访问它。 -
uint8_t *p=(uint8_t *)&NOCAN_REGS;
等强制转换用于序列化目的 - 将较大的类型转换为字节流。大多数SPI通信和外部CAN控制器都使用字节。通过获取结构体&NOCAN_REGS
的地址并将其转换为uint8_t
指针,我们可以逐字节访问结构体。因此,在C中,这种强制转换通常会引发严重问题,但是有一条特殊规则(ISO C 6.3.2.3),允许我们使用字符类型的指针来检查C中的任何类型。在所有合理的主流系统上,uint8_t
只是unsigned char
的别名,因此它是字符类型。这意味着这个强制转换是有效的,逐字节引用结构体也是有效的。(相反,通过使用结构体指针从字符类型数组到结构体的转换是不允许/未定义行为的。) -
此类未定义行为的示例是
(uint16_t*)CHIP_UDID
、(uint16_t*)NOCAN_REGS.UDID
等强制转换。这里可能发生任何事情,可能会导致不对齐访问和严格别名违规错误。永远不要编写这样的脏强制转换。 -
宏
NOCAN_STATUS_SET
应该在代码审查中引发警报。您不应该为处理对ISR的重入性而禁用全局IRQ掩码,这将损坏MCU上存在的任何其他无关中断的时间和行为。我已经无法计数多少次这样的糟糕驱动程序代码引发了问题。这些应该仅禁用CAN和/或SPI外设的特定中断,而不应在每次访问结构体变量时干扰整个MCU的时序。 -
请注意,CPU的字节序(STM32是小端)与CAN控制器的字节序可能会成为问题。我不知道使用的是哪个控制器,但在序列化期间必须考虑到这一点。总体来说,CAN通常更喜欢大端,并且CAN数据链路层使用大端(标识符,CRC)。
-
在32位MCU上执行时对16位类型使用
~D
等操作是不正确的。你好整数提升:~0x5476
的结果不会变成0xAB89
,而是0xFFFFAB89
。然后被视为负数 dec -21623。整个pseudo_hash
明显是由一个似乎没有掌握C中的整数提升的人草率编写的 - 如果它能工作,那是因为运气。
总体来说,我强烈建议不要使用这段代码。它需要经过比我上面简要审查更深入的审查,并且必须由至少具有中级嵌入式C/微控制器编程技能的人员完成。在那之后,可能可以挽救代码,但至少必须修复不对齐和全局IRQ错误。
英文:
NOTE: The code you link to is very flawed and amateurishly-written with many bugs and bad practices.
Since it mentions SPI, it would appear that the purpose is to control an old external CAN controller like MCP2515 etc using SPI. (Please note that most engineers stopped using these external CAN controllers long before Cortex M and STM32 were even invented.)
To explain what the code does and the problems I could spot only at a glance:
-
NOCAN_REGS
is apparently a struct corresponding to the memory map of the registers in an external CAN controller accessed over SPI. This struct has two oddities: packing was disabled and one member isvolatile
qualified.If this had been an on-chip CAN controller then the whole of it would have to be
volatile
. In this casevolatile
only serves as protection against compiler optimizations when the variable is shared with an ISR. So it's correct but should perhaps be documented better.What's worse is the disabled packing, which means that the
uint32_t
members are likely allocated at misaligned addresses. So accessing them asuint32_t
might cause hard faults. Not sexy. These should have beenuint8_t [4]
arrays. (Or alternatively padding should not have been disabled and the struct should be serialized/deseriazlied on member-by-member basis.) -
NOCAN_REGS
is declared as global variable and exposed to the entirely program, which is very bad practice. It should have been declaredstatic
. It should not be in the header. No other part of the program but the driver should access it, if the program design was sound. -
The
uint8_t *p=(uint8_t *)&NOCAN_REGS;
etc casts are used for the purpose of serialization - turning a larger type into a byte stream. Most SPI communication and external CAN controllers work with bytes. By taking the address of the struct&NOCAN_REGS
and converting it to a pointer touint8_t
, we can access the struct byte by byte. So it's handy for passing on a struct to the SPI hardware.Normally casts like this would be deeply problematic in C, but there is a special rule (ISO C 6.3.2.3) which allows us to inspect any type in C using a pointer to a character type. On all sound mainstream systems,
uint8_t
is just a typedef forunsigned char
so it is a character type. Meaning that the cast is valid and de-referencing the struct byte by byte is also valid. (The opposite - going from an array of character types to a struct, by using a struct pointer, is not allowed/undefined behavior.) -
Examples of such undefined behavior are the casts
(uint16_t*)CHIP_UDID
,(uint16_t*)NOCAN_REGS.UDID
. Anything can happen here, it could lead to misaligned access and strict aliasing violation bugs. Never write dirty casts like these. -
The macros
NOCAN_STATUS_SET
should set off all alarms and blinking red lights during code review. You do not write drivers for a specific hardware peripheral that handle re-entancy to an ISR by disabling the global IRQ mask. This will trash all timing and behavior of any other unrelated interrupt present on the MCU. I've lost count over how many times such rotten driver code have caused problems. These should only disable the specific interrupt for the CAN and/or SPI peripheral, not shoot the whole MCU timing to pieces each time the struct variable is accessed. -
Please note that CPU endianess (STM32 is little endian) vs CAN controller endianess may be an issue here. I don't know which controller that's used, but one has to have this in mind during serialization. CAN overall has a tendency to favour big endian and the CAN data link layer uses big endian (identifier, CRC).
-
~D
etc on a 16 bit type while executing on a 32 bit MCU isn't correct. Hello integer promotion: the result of~0x5476
will not become 0xAB89 but0xFFFFAB89
. Which is then treated as a negative number dec -21623. The wholepseudo_hash
is plain sloppily written by someone who seemingly didn't grasp integer promotions in C - if it works at all it is because of luck.
Overall I would strongly recommend not to use this code. It needs to be reviewed more in-depth than my brief review above, and this needs to be done by someone with at least intermediate embedded C/microcontroller programming skills. After that, it might be possible to salvage the code, but at a bare minimum, the misalignment and global IRQ bugs must be fixed.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论