SAMC21 8/16 bit access to 32 bit registers how/why? - Page 1
Forum section: electronics / general

author: Simon
author_rank: Unknown
timestamp: July 26, 2019, 07:00:11 am
content: I am reading through the SAMC21 datasheet and of course the first peripheral to understand is the ports/pins. It isclaimemed that I can have 8 bit wide and 16 bit wide access to registers. I am confused. Why would I want this? it's a 32 bit CPU that in one operation will do actions on a 32 bit register. The other thing is how? Every 8 bits seems to have it's own address in 32 bit space but how do i tell the compiler what I am doing? say I want to access the highet byte or word how does the compiler know the diferrence between accessing the highest 16/8 bits and the whole 32 bit? again, why would i want to bother? does the compiler sort this out internally if i do the same operation on two adjacent addresses?

author: andersm
author_rank: Super Contributor
timestamp: July 26, 2019, 08:50:33 am
content: Code:uint32_t *register_dword = ...;uint16_t *register_lsw = (uint16_t*)register_dword; // assuming little-endianuint16_t *lregister_msw = (uint16_t*)register_dword+1;uint8_t *register_lsb = (uint8_t*)register_dword;uint8_t *register_msb = (uint8_t*)register_dword+3;If you can't think of a use for it, don't worry about it too much.

author: Siwastaja
author_rank: Super Contributor
timestamp: July 26, 2019, 09:11:47 am
content: Yes, by casting to a different pointer type, then dereferencing that pointer.Or, specifying different possible types as an union to the same memory address.C is a strongly typed language. 99% of the time, this makes things clearer. But in some rare cases, like this, you don't have one single type, and then the syntax is longer.On assembly level, it looks neater and maybe more understandable - the memory-mapped peripheral registers won't havetypes, so there is no need for multiple definitions, or conversions - you just use the different write instruction, and choose the suitable (write 32 bits, write 8 bits...) each time. The compiler of course just does this, choose the right instruction based on the type it sees.Rarely needed, but some peripherals actually include the access-width as an element that actually communicates the intent, and does different things based on access width! STM32 SPI peripheral is one such example: if you do a 32-bit write, it adds four elements to the FIFO (if the FIFO is configured to 8 bits per element). But if you just need to send one byte, and simply write:SPI1->TXDR = 123;, you do a 32-bit write (because SPI1->TXDR is defined as volatile uint32_t, and hence is 32 bits wide). This accidentally causes three extra zero elements to be sent out.So you need to cast the register to uint8_t type first:(*(volatile uint8_t*)&SPI1->TXDR) = 123;(Why volatile? Because the peripheral memory-mapped registersneedvolatile qualifier to work at all (you need the writes/reads to happen in the correct order, at the correct place in code), and they are defined volatile in original headers. So when you convert them to another type, don't discard that volatile qualifier. Luckily, at least with GCC, you get a warning if you do that.)And now you have written one byte.The vendor could implement the peripheral library so that it used unions for different access types:SPI1->TXDR.WR32 = 0xacdcabba;orSPI1->TXDR.WR8 = 123;But of course they won't do that. It would be too readable.

author: mikerj
author_rank: Super Contributor
timestamp: July 26, 2019, 09:15:21 am
content: Quote from: Simon on July 26, 2019, 07:00:11 amYou seem to be suggesting that the only variable type you would ever want to use is a 32 bit float or integer? There are specific instructions used to access bytes or 16 bit words because not everything needs or uses 32 bit registers. The compiler will use the appropriate instruction based on the size of the data object you are accessing, so you can e.g. have a pointer to an 8 bit value but set the address to a byte within a 16 or 32 bit word.

author: cv007
author_rank: Frequent Contributor
timestamp: July 26, 2019, 09:46:18 am
content: You have a cpu which has specific instructions for byte/half-word/word access. You have peripheral registers grouped by function (register names), which may be 8/16/32 bits wide. Even though in the strictest sense, a specific nbit access is not always required, it would become awkward if you only had 32bit access (16bit counters, 8bit flags, and so on).The addresses are in bytes, a word is 4 bytes so its address is +4 from the previous word. The 32bit and 16bit instructions need to operate on addresses that are aligned to 4bytes and 2bytes respectively.If using Atmel headers, the size of the register you want to access has already been decided for you.The compiler knows how to produce specific instructions for uint8_t*/uint16_t*/uint32_t*, which your headers are ultimately using.Grab the Arm cortex-whatever user manual or find a summary of instructions somewhere to get an idea of what instructions it has to work with. If wanted.I think I mentioned this before somewhere, but this is one thing they could improve on these sam's- they have address space forever, but chose to create these peripheral registers in various sizes (I'm sure they already had previous peripheral designs and just chose to reuse them, or something along those lines). In contrast, something like a pic32 is quite nice, as each register is 32bits along with a set/clr/inv register for each (register spacing is 16bytes), although there are still times 8/16bit access is used or may be required. In any case, it probably makes little difference if using a manufacturers headers.

author: andersm
author_rank: Super Contributor
timestamp: July 26, 2019, 10:03:35 am
content: Quote from: mikerj on July 26, 2019, 09:15:21 amI understood the question as, why are 8-, 16-, and 32-bit accesses allowed to thesameregister.

author: Simon
author_rank: Unknown
timestamp: July 26, 2019, 10:25:15 am
content: so basically I would send 8, 16 or 32 bit values to the addresses of the 8 bit chuncks and those bits will be written so if I send 8 bits to an 8 bit chunck address only that address is written but if I send 16 or 32 bits it just keeps going until it runs out of bits.But 24 bits is not possible so is that upper 16 bits and lower 16 bits at a time only ?Ultimately I suppose the compiler deals with it.

author: cv007
author_rank: Frequent Contributor
timestamp: July 26, 2019, 11:09:51 am
content: Code:#include <stdint.h>int main(){uint32_t n = *(volatile uint8_t*)0x1000000;n = *(volatile uint16_t*)0x1000004;n = *(volatile uint32_t*)0x1000008;*(volatile uint8_t*)0x1000000 = 0xFF;*(volatile uint16_t*)0x1000004 = 0xFFFF;*(volatile uint32_t*)0x1000008 = 0xFFFFFFFF;for(;;){}}/*00000000 <main>:0: e3a03401 mov r3, #16777216 ; 0x10000004: e5d32000 ldrb r2, 8: e1d320b4 ldrh r2, [r3, #4]c: e5932008 ldr r2, [r3, #8]10: e3e02000 mvn r2, #014: e5c32000 strb r2, 18: e1c320b4 strh r2, [r3, #4]1c: e5832008 str r2, [r3, #8]20: eafffffe b 20 <main+0x20>*/Notice the b and h for byte and half-word access. I told the compiler what I wanted by the pointer type, and it did the rest. When using the Atmel headers, the pointer type is already provided for you and you will get what you want. You don't need to worry about it, just know that it exists.

author: Siwastaja
author_rank: Super Contributor
timestamp: July 26, 2019, 11:35:45 am
content: Quote from: Simon on July 26, 2019, 10:25:15 amIt's all about code size and speed optimization (and in some cases, atomicity as well!)You can write 4 bytes with 8-bit write instructions, but it takes 4 times longer, and requires 4 times more code space for the instructions. 32-bit write instruction does it in one go.But if youneedto just write 8 bits, you have two choices:1) Just write that 8 bits, using a 8-bit write instruction2) Read out 32 bits, modify only the 8 bits, and write 32 bits back. (read-modify-write).If an 8-bit write didn't exist, you would be forced to use the second way. This would be not only slower, but also prone to race conditions, if you have any form of multitasking going on.Remember that many peripheral registers are triggered on write or read operations to do something. Read-modify-write may be forbidden, or produce wrong results, on such registers.So it's great that way #1 exists.Yes, practically all instruction set architectures lack 24-bit accesses. So if you need to write 24 bits, you have two options:1) Write 16, write 8. Two memory operations.2) Read-modify-write 32 bits. Two memory operations.Totally depends on case, and race condition requirements which way is better. But such a 24-bit access would be very rare. Never seen that in a peripheral register.And if your own code needs 24-bit storages, you usually just use 32 bits. Very seldom you are so memory limited, but indeed that does sometimes happen.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 26, 2019, 08:52:40 pm
content: Look atpage 22 of the datasheet, the pinout for the SAMC21E/SAMC20E VQFN32/TQFP32 package: It has a single pin port, port A, with pins PA00..PA11, PA14..PA19, PA22..25, PA28, PA29, PA30, and PA31.Let's say the address of port A data output register is0x41000010. (That's just a guess, as the datasheet does not tell the actual address, only just the base, and otherwise just refers to their own C HAL, ASF. How rude.)To set pins PA00, PA01, PA02, PA03, PA04, PA05, PA06, and PA07, use*(volatile uint8_t *)0x41000010 = bits;.To set pins PA08, PA09, PA10, PA11, (ignored), (ignored), PA14, and PA15, use*(volatile uint8_t *)0x41000011 = bits;.To set PA00..PA11 and PA14 and PA15, use*(volatile uint16_t *)0x41000010 = bits;.If some of those pins are inputs or tristated (neither inputs nor outputs), the corresponding bits are ignored (and read as zeros), just like the bits for PA12 and PA13.Let's say you wish to interface to a 2.8" 320×240 display with an IPS panel and an ILI9325 controller, with a 9-bit parallel interface. You pick pins PA00 through PA08. You also pick PA09 as command/data select pin, PA10 as register select pin, and use PA11, PA14 and PA15 as input pins (or do not use them at all). Then, you can use*(volatile uint16_t *)0x0x41000010 = bits;to set those nine pins, command/data, and register/select using a single cycle -- and even do DMA to it (using 16-bit transfers; BEATSIZE=2). (A single DMA buffer can this way contain both commands and data.)Let's say you use PA24, PA25, PA28, PA29, PA30, and PA31 for some parallel inputs, maybe six input pins of a keyboard matrix. You can read them all in a single cycle usingbits = *(volatile uint8_t *)0x41000013;. You can even do DMA I/O on them concurrently when DMA'ing data to the display, since SAMC21 has lots of DMA channels. To receive data, you do need to send a read command, then set PA00..PA08 as inputs, but for the data part itself you can use DMA.On ARM Cortex M0s and variants with more than one 32-bit port, even if just a few bits are usable on each port, picking your pin use smartly opens up a lot of ways how the DMA engine can do free work for you, and reduce the number of cycles used when doing simple I/O. I don't know about commercial products, but at least my own projects seem to involve a lot of I/O. (Right now, I have an itch to look at SAM4S for use as a programmable blitter, a slave graphics processor, for exactly the abovementioned display. Cheap in 48-pin LQFP packages with lots of RAM, hardware UART/I2C/SPI for comms with master processor, and enough pins for 16/18-bit color.)

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 27, 2019, 06:16:44 pm
content: It will be useful everytime you want to write or read a group of adjacent IOs as an 8-bit, or 16-bit word. Plenty of uses here when dealing with parallel busses.Of course if you're only going to use the IOs as individual bits or as a whole 32-bit port, you won't see a benefit.You may say that on ARM processors, you can still access several bits at the same time, possibly modifying only the bits you want in one register write. But not as a bare data write, only in a bit set/bit clear way. Writing data words in this way would be very inefficient, wouldn't it?

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 27, 2019, 08:55:39 pm
content: When accessing (reading, setting, clearing, or toggling) a single pin, byte access using Cortex-M0 instruction set should often yield more compact code.For example, when you set pin 23 of port A on SAMC21, you do not actually need to do*(volatile uint32_t *)0x41000018 = 1 << 23;. It'd compile toCode:0: 2280 movs r2, #1282: 4b02 ldr r3, [pc, #8]4: 0412 lsls r2, r2, #166: 601a str r2, :c: 41000018 .word 0x41000018It is better done using*(volatile uint8_t *)0x4100001a = 1 << 7;as it has the exact same effect, but should compile toCode:0: 2280 movs r2, #1282: 4b02 ldr r3, [pc, #8]4: 701a strb r2, :c: 41000018 .word 0x4100001ai.e. the shift left by 16 bits (lsls r2, r2, #16) is not needed.I don't have a SAMC21 at hand, but I do have a Teensy LC with a Freescale MKL26Z64VFT4 MCU, also Cortex-M0+. For this one, I do know the actual 32-bit registers:GPIOx_PDOR: Port x data output register.When written to, sets the output pins to states matching the value. Bits corresponding to input/unbounded pins are ignored.When read from, input and tristate bits yield an undefined value.GPIOx_PDIR: Port x data input register.When read from, each bit in the output corresponds to an input pin state. Output and unbounded pins read as zeroes. Read-only.GPIOx_PSOR: Port x data set register.When written to, sets each output pin corresponding to a set bit high. Pins corresponding to zero bits are left unchanged. Reads zero.GPIOx_PCOR: Port x data clear register.When written to, clears each output pin corresponding to a set bit. Pins corresponding to zero bits are left unchanged. Reads zero.GPIOx_PTOR: Port x data toggle register.When written to, toggles the state of each output pin corresponding to a set bit. Pins corresponding to zero bits are left unchanged. Reads zero.GPIOx_PDDR: Port x data direction register.A set bit corresponds to an output pin, and a clear bit to an input pin.PORTx_PCRn: Pin control registernfor portx.These control the pin multiplexing, interrupts, filters, slew rate, and pullup/down.There are alsoFGPIOx_PyzRfast GPIO register aliases, that have a different address, but complete in a single cycle. For this particular chip, there are five ports (x):A,B,C,D, andE, although there are only 36 GPIO pins or so. All above registers are 32-bit, but support 8- and 16-bit accesses too.There are also two write-only registers,PORTx_GPCLRandPORTx_GPCHR, that only support 32-bit accesses. The high 16 bits specify the pins (set bit corresponding to output pins whose state is affected), and the low 16 bits specify their corresponding states.PORTx_GPCLRaffects pins 0-15, andPORTx_GPCHRpins 16-31. These are especially powerful, because a single 32-bit write can set and clear either the low or high 16 pins in a single port.It is obvious that if we group related pins in the same port, we can modify/read their states extremely efficiently.8-bit and 16-bit accesses are useful, because they let us divide the port into four or two groups of pins, that can be manipulated completely independently.In my case, the board exposes pins 1 and 2 of port A, pins 0, 1, 2, 3, 16, and 17 of port B, pins 0 to 7 of port C, pins 0 to 7 of port D, and pins 20, 21, 24, 25, 29, and 30 of port E. Byte access to least significant byte of port A allows me to control PA0..PA3 independently of PA16,PA17 (byte access to second-most significant byte of the same register). Similarly, byte access to port E separates the pins into two groups, pins 20 and 21, and pins 24, 25, 29, and 30.So, essentially, I have three independent groups of two consecutive pins (PA1,PA2; PA16,PA17; and PE20,PE21), two independent groups of four pins (PB0-PB3, and PE24,25,29,30), and two independent groups of eight consecutive pins (PC0-PC7, PD0-PD7).It makes design (choosing which pins to use for what, especially since there are alternate uses for almost every pin) harder, in the sense that to really squeeze everything out from such a chip, you need to think about the use cases before writing the firmware. I consider that a fun part, myself.Looking at thedatasheet for the SAM C20/C21 familyin detail, it does say that PORT aperture starts at 0x41000000 on page 43, and 28.7 (page 491) lists the offsets. The base address for port A is 0x41000000, 0x41000080 for port B, 0x41000100 for port C, 0x41000180 for port D, 0x41000200 for port E, and so on.The registers are very much similar, just named a bit differently: Microchip usesOUTforGPIOx_PDOR,INforGPIOx_PDIR,DIRforGPIOx_PDDR,OUTCLRforGPIOx_PCOR,OUTSETforGPIOx_PSOR,OUTTGLforGPIO_PTOR, and so on. It has additional registers to set specific pins in a port as outputs (DIRSET), inputs (DIRCLR), or even toggle the direction (DIRTGL), but I don't see ones matchingPORTx_GPCHRorPORTx_GPCLR.This means thatGPIOA_PDORon SAMC20 and SAMC21 is at address 0x41000010,GPIOB_PDORat 0x41000090, and so on.While there are differences, the logical idea/pattern behind GPIO pin access is the same as for the one I described for another Cortex M0+ MCU (Teensy LC), above in this post.Of course, if you use Microchip's ASF in Atmel Studio, you won't have direct access to those.

author: KL27x
author_rank: Super Contributor
timestamp: July 28, 2019, 07:06:48 pm
content: Data compression vs processing speed. Takes more instructions to pack and unpack, but you get 4x the storage space, if your data table/stream is using only 8 bits.

author: Simon
author_rank: Unknown
timestamp: July 28, 2019, 07:10:06 pm
content: why? if your processor and system is 32 bits then why is 8 bit access any faster than 32 bit?

author: ataradov
author_rank: Super Contributor
timestamp: July 28, 2019, 07:21:14 pm
content: Quote from: Simon on July 28, 2019, 07:10:06 pmIf you need to extract bits 8-15, for example from GPIO registers, it is faster to read a byte rather than do shifts and masks after reading the whole word. Even more savings on writes. But this is a very contrived example.Really, there is no real advantage to that and 8- and 16-bit registers are a pain. Those chips are designed this way for legacy reasons, and I hope that this stuff goes away on new devices.It is better to actually design good virtual ports, since it is the only good use for those strange access types.

author: Simon
author_rank: Unknown
timestamp: July 28, 2019, 07:30:13 pm
content: i see yes eliminating the math on a 32 bit number to get the 8 or 16 bits out is a good idea. I still truggle to see though how I do this in C unless i address specific addresses unless the headers have extra definitions like:registerregister-aregister-bregister-abregister-cregister-dregister-cd

author: ataradov
author_rank: Super Contributor
timestamp: July 28, 2019, 07:33:37 pm
content: Read PA[15:8]: "uint8_t value = *(((uint8_t *)&PORT->Group.IN.reg) + 1);". This is not pretty, but you don't have to do this a lot, so it should be fine.

author: Simon
author_rank: Unknown
timestamp: July 28, 2019, 07:42:01 pm
content: yea, i think i will do it in 32 bit read/writes. I really do not need speed and with the SET/CLR registers it's a doddle anyway.

author: ataradov
author_rank: Super Contributor
timestamp: July 28, 2019, 07:43:14 pm
content: It is nice to have an option. Especially with DMA as was mentioned earlier. This may be your only viable option in that case.

author: KL27x
author_rank: Super Contributor
timestamp: July 28, 2019, 08:33:31 pm
content: With 8 bit PIC, we have swapf, to swap the high 4 bits with the low 4 bits. So to store and retrieve 4 bit data without wasting half the memory, you can write (well, mask then ior) to the register, swapf, write. And to retrieve, you have to do the reverse and mask to remove the top nibble. There are dozens of other ways you could do that, but this, as cumbersome as it is, is still probably the fastest. It would surely be nicer and faster to be able to store and retrieve them to the accumulator, directly, with the nibble-analogy of those fancy instructions you have.The chip would have a hardware feature in silicon, to do this in a single instruction, which saves program memory and reduces the clock cycles for execution. At the cost of dedicating one address of its core to doing just this.

author: ataradov
author_rank: Super Contributor
timestamp: July 28, 2019, 08:38:40 pm
content: Even Cortex-M0+ has rev, rev16, revsh and uxt* instructions that make byte swaps easier. But yes, obviously it is better to not have to do that at all.

author: cv007
author_rank: Frequent Contributor
timestamp: July 29, 2019, 12:48:06 am
content: >yea, i think i will do it in 32 bit read/writes. I really do not need speed and with the SET/CLR registers it's a doddle anyway.Are you using the atmel headers or doing this yourself? If using the atmel headers, the headers do the choosing by their struct organization. (They also have defines in the headers which provide addresses, bit positions, etc., where you can 'bypass' the struct method they have, but you are on your own in choosing how you want to treat these registers).If not using the structs in the atmel headers, you will not get very far into the Port chapter when you will have to make decisions. Hitting the 32bit OUTSET register is easy enough since you can simply shift left the pin number to get your write value. Now go to the PINCFG registers, which are consecutive 8bit registers ordered by pin number. Now try to do 32bit access and you will have quite a contraption on your hands. Treat the PINCFG register as 8bits and its easy. And don't forget about those flag registers if wanting to stick to 32bits, as you will have to always keep in mind where those kinds of registers are located which may not be so friendly to a RMW.The underlying theme is not how to always get the most efficient instructions to carry out a task, but how to communicate to the compiler what you want in a way that you can read and understand (a week later, also). Since the OUTSET register was grouped into 32bits (the port), it makes little sense to start treating it as anything other even though 8bit access will or may be faster since it can load an immediate value into a register (see below disassembly). Same case for other registers that are grouped differently- counters, flag registers, etc., it makes no sense to treat them other than in a 'logical' way whether 32/16/8 bits.Simple example-Code:#include <stdint.h>int main(){#define PORT_BASE 0x6000000#define PA11 (11)#define OUTSET (PORT_BASE+24)#define PINCFG_BASE (PORT_BASE+64)#define PULLUP_BM (4)//set pin 32bit - easy*(volatile uint32_t*)OUTSET = 1<<PA11;//set pin 8bit - not so easy*(volatile uint8_t*)(OUTSET+(PA11/8)) = 1<<(PA11%8);//pullup on 8bit - easy*(volatile uint8_t*)(PINCFG_BASE+PA11) |= PULLUP_BM;//pullup on 32bit - not so easy (this may not even be correct)((volatile uint32_t*)(PINCFG_BASE))[PA11/4] |= PULLUP_BM<<((PA11%4)*8);for(;;){}}/*//set pin high//32bit access//*(volatile uint32_t*)(0x6000000+OUTSET) = 1<<PA11;00000000 <main>:0: 4b02 ldr r3, [pc, #8] ; (c <main+0xc>)2: 2280 movs r2, #128 ; 0x804: 0112 lsls r2, r2, #46: 601a str r2, [r3, #0]8: e7fe b.n 8 <main+0x8>a: 46c0 nop ; (mov r8, r8)c: 06000018 .word 0x06000018//set pin high//8bit access//*(volatile uint8_t*)(0x6000000+OUTSET+(PA11/8)) = 1<<(PA11%8);00000000 <main>:0: 4b01 ldr r3, [pc, #4] ; (8 <main+0x8>)2: 2208 movs r2, #84: 701a strb r2, [r3, #0]6: e7fe b.n 6 <main+0x6>8: 06000019 .word 0x06000019//set pullup on//8bit access//*(volatile uint8_t*)(PINCFG_BASE+PA11) |= 1<<PULLUP_BP;00000000 <main>:0: 4a02 ldr r2, [pc, #8] ; (c <main+0xc>)2: 7813 ldrb r3, [r2, #0]4: 2104 movs r1, #46: 430b orrs r3, r18: 7013 strb r3, [r2, #0]a: e7fe b.n a <main+0xa>c: 0600004b .word 0x0600004b//set pullup on//32bit access((volatile uint32_t*)(PINCFG_BASE))[PA11/4] |= PULLUP_BM<<((PA11%4)*8);00000000 <main>:0: 4a03 ldr r2, [pc, #12] ; (10 <main+0x10>)2: 6811 ldr r1, [r2, #0]4: 2380 movs r3, #128 ; 0x806: 04db lsls r3, r3, #198: 430b orrs r3, r1a: 6013 str r3, [r2, #0]c: e7fe b.n c <main+0xc>e: 46c0 nop ; (mov r8, r8)10: 06000048 .word 0x06000048*/In one case it is much easier to code and think about treating the register as a 32bit register, in the other it is easier to treat as 8bits. In both cases it may not always be the most efficient way, but no one wants to continually micromanage the compiler at that level.Bottom line- if using the atmel header structs, mostly there is nothing to think about. If creating your own methods, you are going to use what is easiest, and it won't take long to figure that out.edit-could be code errors, but you get the idea

author: westfw
author_rank: Super Contributor
timestamp: July 30, 2019, 05:16:24 am
content: You should be able to do:Code:#define BYTEACCESS(reg32, offset) ( *(offset + ((volatile uint8_t*)(&(reg32)))) )uint8_t c2 = BYTEACCESS(REG_PORT_OUT0, 2);BYTEACCESS(PORT->Group.OUTCLR.reg, 2) = c2;Which will make it a lot cleaner. In order:Code:&(reg32) // The Atmel-named registers reference contents; get address instead.(volatile uint8_t*)... // cast it to a byte pointer.(offset + ... // add in the byte offset (0-3)*(...) // back to contentsgcc for arm seems to insert an extra "uxtb" instruction; I'm not sure why.