Search code examples
cpointersmicrocontroller

Understanding pointer definition in C


I have a general knowledge of pointer and their use but I come up to some code that uses pointer casting (I think) I never saw and I'm not sure I understand

The code is a function in C (written for a STM32F091 microcontroller) that reads data from an EEPROM (93c66).

From what I'm understanding the function takes the address to read as an argument. The address value is used as a pointer and sent on the SPI.

If the communication is correctly complete, it starts listening from the actual value sent from the EEPROM and then the function return the data

unsigned short rd_word(unsigned short add93)
{
  unsigned char *pt = (unsigned char *)(&add93);
  unsigned char error = 0;
  
  CS_HIGH;
  
  add93 &= 0x00ff;
  add93 |= 0x0600;
  
  while (HAL_SPI_Transmit(&hspi2, pt, 1, 1000) != HAL_OK){
    __iar_builtin_no_operation();
  }

  SET_BIT((&hspi2)->Instance->CR1, SPI_CR1_CPHA);
  
  while (HAL_SPI_Receive(&hspi2, pt, 1, 1000) != HAL_OK){
    __iar_builtin_no_operation();
  }
  
  CLEAR_BIT((&hspi2)->Instance->CR1, SPI_CR1_CPHA);

  
  CS_LOW;
  
  return add93;
}

My question is what the code unsigned char *pt = (unsigned char *)(&add93); does? From what I'm understanding it casts add93 to an unsigned char pointer?

Now, if unsigned char *pt is a pointer, so it's value is the actula memory value where add93 is stored, why is it transfered the pt and not add93?

The code

add93 &= 0x00ff;
add93 |= 0x0600;

cast add93 in an unsigned char (add93 &= 0x00ff;) and attach the inital read code for the eeprom (add93 |= 0x0600;) [0x6 = 110b]

SPI interface settings

/* SPI2 init function */
void MX_SPI2_Init(unsigned int dataSize)
{

  /* SPI2 parameter configuration*/
  hspi2.Instance = SPI2;
  hspi2.Init.Mode = SPI_MODE_MASTER;
  hspi2.Init.Direction = SPI_DIRECTION_2LINES;
  hspi2.Init.DataSize = SPI_DATASIZE_16BIT;
  hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
  hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
  hspi2.Init.NSS = SPI_NSS_SOFT;
  hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128;
  hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi2.Init.CRCPolynomial = 7;
  hspi2.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
  hspi2.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
  if (HAL_SPI_Init(&hspi2) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

}

Solution

  • Pointers are equivalent to address most of the time. But in embedded systems they are usually local addresses inside the microcontroller itself. Here you are dealing with some sort of external memory.

    rd_word is one of the functions for the driver interfacing this memory. Apparently they decided that it should have a parameter unsigned short add93 corresponding to an address in that memory, but inside your microcontroller it is expressed as a plain number.

    Here we may note that using unsigned short and similar "primitive data types" that come with C per default is bad practice, particularly in embedded systems. Because we don't know how large this type is - maybe it is 16 bits. Better practice as done in professional software is to use the types from stdint.h like uint16_t.

    The code wishes to translate the add93 address to something meaningful for the memory hardware and send it over SPI. And SPI almost always works byte by byte, 8 bits at a time. So we need to serialize add93 into byte chunks in order to be able to send it.

    C has a special rule that allows us to inspect any variable type byte by byte. We may do so by casting the address of any variable into a pointer to character type, the byte type of C. Most often expressed as either unsigned char or uint8_t, because when dealing with raw bytes we do not want any signedness to mess things up.

    &add93 takes the address of the local variable add93 for this purpose. This variable sits on the local stack in the microcontroller. By casting the address to pointer to a character (unsigned char *)(&add93), we get an address to the first byte. Some things of note here:

    • The inner parenthesis (&add93) is not necessary.
    • The resulting pointer will point at the lowest address of the unsigned short. If that corresponds to the most significant byte or least significant byte depends on CPU endianness (What is CPU endianness?). Cortex M and STM32 are pretty much exclusively little endian, so we have the least significant byte here.
    • Knowing about endianness is fundamental to embedded systems programming. All communication protocols and peripheral hardware like your memory may use a different endianness than your CPU. Big endian is very common for network protocols for example. If we are interfacing with a network or device using a different endianness, we have to shuffle the byte order around.

    This stuff here is just some bit-wise arithmetic:

    add93 &= 0x00ff;
    add93 |= 0x0600;
    

    The first row taking bitwise AND against a mask with all ones set 0xFF... is known as "masking", meaning that the lowest byte is preserved and the highest byte is cleared in this case. Then they add the value 0x6 to the highest byte. We may recall that pt points at the lowest byte, so the value it points at corresponds to the part that was preserved by the bit masking.

    The SPI send function HAL_SPI_Transmit(&hspi2, pt, 1, 1000) supposedly sends 1 single byte here, the one we pointed at.

    SET_BIT((&hspi2)->Instance->CR1, SPI_CR1_CPHA); changes the clock phase of the SPI communication on the fly, for whatever reason. It is exotic to do this in the middle of transmission, but then there are lots of weird SPI interfaces out there, it is poorly standardized.

    It then receives 1 byte which supposedly overwrites the data where pt points at. This is a bit peculiar too - we may recall that SPI is full duplex always sending while receiving. So if we send something first and then receive something, we actually send 2 bytes and receive 2 bytes. I don't know the details of the SPI driver here or why we would (not) like to send 2 bytes.

    We may however note that the most significant byte set by add93 |= 0x0600 has not been sent. Which is weird. Rather, the function will return a mix of the 0x06 and whatever was received from SPI.

    It is strongly recommended to view the data actually getting sent with an oscilloscope. Trigger on the chip select signal and view MOSI and/or MISO.


    To address your specific questions:

    My question is what the code unsigned char *pt = (unsigned char *)(&add93); does? From what I'm understanding it casts add93 to an unsigned char pointer?

    No, as mentioned above it casts the address of add93 to a byte pointer, so that we may access the unsigned short byte by byte. This is an address in your microcontroller, not to be confused with the address of whatever EEPROM part you are dealing with.

    Now, if unsigned char *pt is a pointer, so it's value is the actula memory value where add93 is stored, why is it transfered the pt and not add93?

    Because the SPI driver functions like HAL_SPI_Transmit expect a byte pointer to an allocated buffer as input. It needs to work byte by byte. They could have passed (unsigned char*)&add93 to the function as well and that would have been equivalent. But they couldn't just pass &add93 because that would result in an unsigned short* which is not a compatible type with unsigned char*.

    Contrary to popular belief, wild and crazy pointer conversions are not really allowed in C and getting them correct is tricky. The general advise to beginners is to pretty much never cast at all, because it's so easy to go wrong with it.

    cast add93 in an unsigned char (add93 &= 0x00ff;

    No there was no cast anywhere, this worked directly on the unsigned short variable.

    attach the inital read code for the eeprom (add93 |= 0x0600;

    That seems to have been the intention, but it doesn't look like that part was ever sent over SPI at all. This code is not well-written and possibly quite buggy. Maybe they intended to increment pt to point at the next byte at some point, but it doesn't look like it. Spontaneously I'd say that the code is just plain broken, but I'd have to take a closer look at the SPI driver to tell for sure.