Low-level programming - Registers - SAMD51

Hello everyone!

I'm using a SAMD51 based board and I'm trying to understand how it really works. So far, I've done several tasks with it using the Arduino IDE and many of the libraries made by the community.

I come from a background in high-level programming, so I took my time learning a bit more of embedded programming using cpp and c. However, I'm trying to do some stuff that needs to understand and manipulate the board registers.

I'm trying to face a simple problem before increasing the difficulty and for this, I choose to understand this code that reads the SAMD51 temperature as it has built-in capability to do so.

I've tested this and it did work. However, as mentioned before, I want to understand it.

Let's take this part, as an example:

  SUPC->VREF.reg |= SUPC_VREF_TSEN | SUPC_VREF_ONDEMAND;
  ADC0->CTRLB.bit.RESSEL = ADC_CTRLB_RESSEL_12BIT_Val;

I can't find SUPC_VREF_TSEN on the datasheet. But I do find the separated, like SUPC, VREF, TSEN. In toppic 19. SUPC – Supply Controller there is a table 19.7 Register Summary that shows VREF and TSEN:

From my understanding, looks like that in the SUPC register, in the 32bit section related to the VREF, it changes the TSEN (2nd bit) and ONDEMAND (7th bit) to 1 with the SUPC_VREF_TSEN | SUPC_VREF_ONDEMAND while keeping the others unchanged due to the |=.

Is this correct?

Does anyone have good recommendations to keep reading/learning about this? Like a book, blog, post, youtube, ...

Full code

// m4 SAMD51 chip temperature sensor on ADC
// Decimal to fraction conversion. (adapted from ASF sample).
//#define NVMCTRL_TEMP_LOG              (0x00800100)  // ref pg 59
#define NVMCTRL_TEMP_LOG NVMCTRL_TEMP_LOG_W0

static float convert_dec_to_frac(uint8_t val) {
  float float_val = (float)val;
  if (val < 10) {
    return (float_val / 10.0);
  } else if (val < 100) {
    return (float_val / 100.0);
  } else {
    return (float_val / 1000.0);
  }
}

static float calculate_temperature(uint16_t TP, uint16_t TC) {
  uint32_t TLI = (*(uint32_t *)FUSES_ROOM_TEMP_VAL_INT_ADDR & FUSES_ROOM_TEMP_VAL_INT_Msk) >> FUSES_ROOM_TEMP_VAL_INT_Pos;
  uint32_t TLD = (*(uint32_t *)FUSES_ROOM_TEMP_VAL_DEC_ADDR & FUSES_ROOM_TEMP_VAL_DEC_Msk) >> FUSES_ROOM_TEMP_VAL_DEC_Pos;
  float TL = TLI + convert_dec_to_frac(TLD);

  uint32_t THI = (*(uint32_t *)FUSES_HOT_TEMP_VAL_INT_ADDR & FUSES_HOT_TEMP_VAL_INT_Msk) >> FUSES_HOT_TEMP_VAL_INT_Pos;
  uint32_t THD = (*(uint32_t *)FUSES_HOT_TEMP_VAL_DEC_ADDR & FUSES_HOT_TEMP_VAL_DEC_Msk) >> FUSES_HOT_TEMP_VAL_DEC_Pos;
  float TH = THI + convert_dec_to_frac(THD);

  uint16_t VPL = (*(uint32_t *)FUSES_ROOM_ADC_VAL_PTAT_ADDR & FUSES_ROOM_ADC_VAL_PTAT_Msk) >> FUSES_ROOM_ADC_VAL_PTAT_Pos;
  uint16_t VPH = (*(uint32_t *)FUSES_HOT_ADC_VAL_PTAT_ADDR & FUSES_HOT_ADC_VAL_PTAT_Msk) >> FUSES_HOT_ADC_VAL_PTAT_Pos;

  uint16_t VCL = (*(uint32_t *)FUSES_ROOM_ADC_VAL_CTAT_ADDR & FUSES_ROOM_ADC_VAL_CTAT_Msk) >> FUSES_ROOM_ADC_VAL_CTAT_Pos;
  uint16_t VCH = (*(uint32_t *)FUSES_HOT_ADC_VAL_CTAT_ADDR & FUSES_HOT_ADC_VAL_CTAT_Msk) >> FUSES_HOT_ADC_VAL_CTAT_Pos;

  // From SAMD51 datasheet: section 45.6.3.1 (page 1327).
  return (TL * VPH * TC - VPL * TH * TC - TL * VCH * TP + TH * VCL * TP) / (VCL * TP - VCH * TP - VPL * TC + VPH * TC);
}

float get_tempc() {
  // enable and read 2 ADC temp sensors, 12-bit res
  volatile uint16_t ptat;
  volatile uint16_t ctat;
  SUPC->VREF.reg |= SUPC_VREF_TSEN | SUPC_VREF_ONDEMAND;
  ADC0->CTRLB.bit.RESSEL = ADC_CTRLB_RESSEL_12BIT_Val;
  while (ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_CTRLB); //wait for sync
  while ( ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_INPUTCTRL ); //wait for sync
  ADC0->INPUTCTRL.bit.MUXPOS = ADC_INPUTCTRL_MUXPOS_PTAT;
  while ( ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_ENABLE ); //wait for sync
  ADC0->CTRLA.bit.ENABLE = 0x01;             // Enable ADC

  // Start conversion
  while ( ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_ENABLE ); //wait for sync

  ADC0->SWTRIG.bit.START = 1;

  // Clear the Data Ready flag
  ADC0->INTFLAG.reg = ADC_INTFLAG_RESRDY;

  // Start conversion again, since The first conversion after the reference is changed must not be used.
  ADC0->SWTRIG.bit.START = 1;

  while (ADC0->INTFLAG.bit.RESRDY == 0);   // Waiting for conversion to complete
  ptat = ADC0->RESULT.reg;

  while ( ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_INPUTCTRL ); //wait for sync
  ADC0->INPUTCTRL.bit.MUXPOS = ADC_INPUTCTRL_MUXPOS_CTAT;
  // Start conversion
  while ( ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_ENABLE ); //wait for sync

  ADC0->SWTRIG.bit.START = 1;

  // Clear the Data Ready flag
  ADC0->INTFLAG.reg = ADC_INTFLAG_RESRDY;

  // Start conversion again, since The first conversion after the reference is changed must not be used.
  ADC0->SWTRIG.bit.START = 1;

  while (ADC0->INTFLAG.bit.RESRDY == 0);   // Waiting for conversion to complete
  ctat = ADC0->RESULT.reg;


  while ( ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_ENABLE ); //wait for sync
  ADC0->CTRLA.bit.ENABLE = 0x00;             // Disable ADC
  while ( ADC0->SYNCBUSY.reg & ADC_SYNCBUSY_ENABLE ); //wait for sync

  return calculate_temperature(ptat, ctat);
}

void setup() {
  Serial.begin(9600);
  while (!Serial);
  delay(1000);
}

void loop() {
  Serial.println(get_tempc());
  delay(2000);

}

Reference

SAM D5x/E5x Family Data Sheet (microchip.com)
samd51/m4temp.ino at master · manitou48/samd51 · GitHub

You'll have to dig deep into your SAMD51 core installation to find the .h files where those macros, structs, and unions are declared. I don't have that core installed, but in my SAMD21 installation (at least for an Adafruit M0 Feather), it starts with 'samd21g18a.h'. That .h includes many others where all the declarations are found.

I don't have a SAMD51 (but I have a bunch of STM32F4 and STM32F7) and they all kinda work the same.

So think of a peripheral, lets say a TIMER1. That TIMER1 has multiple registers to control it

TIMER1 <-- the peripherals
ISR <--- Interrupt and Status Register
PSC <--- Prescale Register
CNT <--- Counter Register
.. and a bunch more

Most of the time, to configure a register of a peripheral, you address it using the notation

TIMER1->ISR = whatever
TIMER1->PSC = whatever

Now remember, each register itself have different bit fields. For example, the ISR register might have the Interrupt Enable bits and the Status bits. For simplicity, lets assume you have two types of interrupts and two status flags, one for each interrupt

So the ISR register would have something like
* Interrupt enable A bit
* Interrupt enable B bit
* Status flag A bit
* Status flag B bit

The manufacturers make it easy to remember these bit fields by creating some "defines" which are just substitution. In the example above, they might name these bit fields as such

TIMER1_ISR_IEA = (1<<0) -> this describes Interrupt Enable A bit in the ISR register of the TIMER1 peripheral

TIMER1_ISR_IEB = (1<<1) -> this describes Interrupt Enable B bit in the ISR register of the TIMER1 peripheral

This way, you can write to the register with a more meaningful way

TIMER1->ISR = TIMER1_ISR_IEA | TIMER1_ISR_IEB

which is effectively

TIMER1->ISR = (1<<0) | (1<<1)

This becomes very useful when you have 32 bit registers with 20 bit fields to keep track of (which is common with the more advance Cortex MCUs)

Using the above example, which would be easiest to "read" (all have the same effect)

TIMER1->ISR = 0x03
TIMER1->ISR = (1<<0) | (1<<1)
TIMER1->ISR = TIMER1_ISR_IEA | TIMER1_ISR_IEB

1 Like

Fabulous! I found them in the directory C:\Users\myUserName\AppData\Local\Arduino15\packages\adafruit\tools\CMSIS-Atmel\1.2.1\CMSIS\Device\ATMEL\samd51\include\component... I was trying to find them inside de variant folder.
image

About @hzrnbgy explanation, it really was helpful! I will keep digging through those files and making some notes to have a better grasp of it.

I'll try to post my progress here so if anyone has this problem later, they can find some help.