Writing to registers in C without using any library

Go To Last Post
22 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Is it possible to write to registers in C without using any libraries (AVR libc).

Example, instead of DDRB=0xff; with #include <avr/io.h>

 

I want to use the hex value of the register and write my data this way. 

In assembly we use instructions given by datasheet to write values to registers using IN/OUT or whatever the command is. Is this purely possible in C?  Asking for learning purposes, if it is possible i will try to write my own I/O library. 

 

I also asked before about the AVR libc license but that topic got unpublished after i made certain changes to it. The question was if it is free to use AVR libc for distribution, publishing, production. Seems  , i can use it no worries.

https://stackoverflow.com/questions/24988522/accessing-avr-registers-with-c

This topic has a solution.
Last Edited: Mon. Jan 22, 2018 - 03:44 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

yorem_kastor wrote:
Example, instead of DDRB=0xff; with #include

Sure you can.

"DDRB" is a simple #defined macro.

 

The way it usually works is that you #include <avr/io.h>

avr/io.h is just a simple header file you can open in a text editor.

It's contents are not much more than a list of different AVR's

On my linux system it is in: /usr/lib/avr/include/avr/io.h

#include <avr/sfr_defs.h>

#if defined (__AVR_AT94K__)
#  include <avr/ioat94k.h>
#elif defined (__AVR_AT43USB320__)
#  include <avr/io43u32x.h>
#elif defined (__AVR_AT43USB355__)
#  include <avr/io43u35x.h>
#elif defined (__AVR_AT90PWM1__)
#  include <avr/io90pwm1.h>
#elif defined (__AVR_AT90PWM2__)
#  include <avr/io90pwmx.h>
#elif defined (__AVR_ATmega8U2__)
#  include <avr/iom8u2.h>
#elif defined (__AVR_ATmega16M1__)
#  include <avr/iom16m1.h>
#elif defined (__AVR_ATmega16U2__)
#  include <avr/iom16u2.h>

// Many more processors...

If we open the header file for the ATmega8 you'll see that it's also just a bunch of #defines for registers and such:

/usr/lib/avr/include/avr/iom8.h

// Bunch of other definitions
// ...

/* Port B */
#define PINB	_SFR_IO8(0x16)
#define DDRB	_SFR_IO8(0x17)
#define PORTB	_SFR_IO8(0x18)

/* EEPROM Control Register */
#define EECR	_SFR_IO8(0x1C)

// ...
// More definitions...

Now there is just one more thing missing:

Whats that _SFR_IO8 thing ???

It's defined in the file avr/sfr_defs.h, which was also included via io.h.

// ...
#define _SFR_IO8    0
#define _SFR_IO8(io_addr) ((io_addr) + __SFR_OFFSET)
// ...

 

So the number you want is 0x17 Which you can also find in the datasheet.

But you'll also have to take ists adress, cast it to some static const volatile pointer or somethig.

Missed that bit somewhere...

 

But why would you want to do such a thing?

Actually, why not turn it around?

Study those header files a bit and try to deduce why they are written in the way they are.

You'll learn a few things about writing portable code.

 

Doing magic with a USD 7 Logic Analyser: https://www.avrfreaks.net/comment/2421756#comment-2421756

Bunch of old projects with AVR's: http://www.hoevendesign.com

Last Edited: Sun. Jan 21, 2018 - 03:14 PM
This reply has been marked as the solution. 
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 2

Write a small program, just doing e.g. one simple port I/O. Set the "save-temps" option in Atmel Studio: Project Properties, Toolchain, AVR/GNU C Compiler, Miscellaneous and then tick Do not delete temporary files.

Rebuild the project and then look for a file with the same name as your source but with file extension ".i". There's what the compiler sees after all pre-processing has been done.

 

Here's an example:

int main(void)
{
	DDRB = 0xFF;
}

Here's an excerpt of the main.i file build according to the instructions above:


# 11 ".././main.c"
int main(void)
{

# 13 ".././main.c" 3
(*(volatile uint8_t *)((0x04) + 0x20))
# 13 ".././main.c"
     = 0xFF;
}

Ignore all lines beginning with a hash sign (#) and you see that what the compiler proper will process is

(*(volatile uint8_t *)((0x04) + 0x20)) 

     = 0xFF;

So, your new program not relying on avr/io.h will be:

int main(void)
{
	(*(volatile uint8_t *)((0x04) + 0x20)) = 0xFF;
}

or possibly

int main(void)
{
	(*(volatile uint8_t *)(0x24) = 0xFF;
}

 

If you're not quite familiar and comfortable with the Harvard architecture, typecasts and pointers then that requires you to sit down with paper and pen and draw some sketches of memory/address-spaces and pointers. E,.g. you need to figure out why the + 0x20 is there. ;-)

 

Advice: Apart from the learning experience, don't rely on this style of coding.

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

Last Edited: Sun. Jan 21, 2018 - 03:46 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

JohanEkdahl wrote:
Advice: Apart from the learning experience, don't rely on this style of coding.

Besides, pointer method seems not optimal
İ did a test for experience, CVAVR, ATmega328
Pointer method takes 1 more clock

"DDRB = " compiled to
LDI (1 clock)
OUT (1 clock)

"*(volatile char *) (0x24) = " compiled to
LDI (1 clock)
STS (2 clock)

Majid

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

But I bet OP wants make something like a struct with addr and data, that can be written. And then one clk more or less could easy be well spend compare to the flexibility and reduced size.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

m.majid wrote:
Besides, pointer method seems not optimal İ did a test for experience, CVAVR, ATmega328 Pointer method takes 1 more clock

 

"Interesting". For avr-gcc the optimal LDI+OUT is generated for all three cases below:

 

	DDRB = 0xFF;

	(*(volatile uint8_t *)((0x04) + 0x20)) = 0xFF;

	(*(volatile uint8_t *)(0x24)) = 0xFF;

ATmega328PB

avr-gcc.exe (AVR_8_bit_GNU_Toolchain_3.6.0_1734) 5.4.0

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

Last Edited: Sun. Jan 21, 2018 - 07:09 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

yorem_kastor wrote:
if it is possible i will try to write my own I/O library

Apart from the learning experience, what is the goal? Are there any cases where avr-gcc/avrlibc does not result in an optimal code sequence for the basic I/O operations?

 

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

yorem_kastor wrote:
I also asked before about the AVR libc license but that topic got unpublished after i made certain changes to it. The question was if it is free to use AVR libc for distribution, publishing, production. Seems  , i can use it no worries.

Yes, granted that you reproduce it's copyright/license text in whatever you distribute.

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

sparrow2 wrote:

But I bet OP wants make something like a struct with addr and data, that can be written. And then one clk more or less could easy be well spend compare to the flexibility and reduced size.

if that's the case I've previously presented my solution for this.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The OP seems to have the idea that libraries are used for sfr access and that the code generated is non-optimal. As has been demonstrated, the compiler will generate native code as you would in assembler.

If the OP is really worried about licensing, then purchase a commercial compiler.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Kartman wrote:
The OP seems to have the idea that ...

OP only contributed the original post.  I've re-read it several times, and I cannot tell what the aim really is.

 

First, define "library".  Is avr-libc a "library"?  Next, can't DDRB=0xff; be done without AVR-libc?  Why would a chip-include file and subordinate .h be part of a "library"?

 

 

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Assuming that the OP means that they want to write C code without using ANY #include files that they themselves haven't written, I see essentially two ways to do this:

  1. replace the io.h definitions with your own very similar ones:  "#define DD *(volatile unsigned char *)0x2A"  Note that in some compilers (ie gcc), this relies on enough optimization being enabled to cause the "memory references" to be converted into in/out instructions (or whatever) if possible.
  2. write inline assembler functions for the various IO instructions IN, OUT, SBI, CBI, etc.  Manually keep track of which registers are accessible by which instructions (like you do in assembler.)  (Hmm.  Perhaps that assembler functions could be made smart enough to do this.)

 

Note that the .h files defining the various symbols (io.h/etc) are not usually considered "libraries"; libraries are usually external collections of actual code.   The various ioxxx.h files are provided by Atmel and generated automatically from XML chip descriptions, while the avr-libc CODE (things like printf()) is open source software with a lot of community contribution.  You can look at the copyrights; while they have similar "bsd-style" licensing language, the Atmel-provided files are Copyright by Atmel, while the community code has assorted individual names.

 

If you're really horribly worried about licensing issues, then:

  1. Don't forget to worry about libgcc, which is invoked by statements like "a = b * c;"   This actually has a LGPL3 license, which is generally more problematic (especially for embedded systems) than the BSD license used for avr-libc.
  2. Use a commercial compiler
  3. hire a lawyer.  (although frankly, some expensive opinion is probably worth less than 20years of precedent.)

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

It is just a small step to fully understand what is going on in the background. I know most people use C to program AVR, SAM but not bother themselves with these kind of questions because their goal is only the end product. I see this in most people who use libraries all the time such as ardunio programmers, i really do not want to call those people programmer or creators(Not all of them who know what they are doing).... they simply copy libraries, header files over and over and get stuck when something does not work. Me on the other hand would like to know how things are done, well i am not a pro at the moment but i can at least write my own header files for modules, chips by reading datasheets. 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

yorem_kastor wrote:
i really do not want to call those people programmer or creators

I'll skip the discussion about them being programmers or not, but there sure is a lot of creators in the Arduino community. You don't always need to get into all nitty-gritty tech details to be a creator.

 


 

I'm still wondering about the reasons, technical or other, for writing your own "I/O library". Or possibly even what that term means.

 


 

As shown above you can drill into the header files down to the actual non-preprocessor C if you want to know what goes on "in the background". (And do note the trick of having the too chain show the C code after pre-processing but before actual compilation).

 

Your next level then will be the assembler code the compiler produces. ;-)

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

yorem_kastor wrote:
Me on the other hand would like to know how things are done
Then -save-temps is a lot of what you need to know about. Nothing in C is really secret. If you want to you can look at <avr/io.h> and see what it does and then what <avr/ioSOME_DEVICE.h> does and so on. For example in:

#include <avr/io.h>

int main(void) {
    DDRB = 0x55;
}

built for a mega16 (say) you will find first that -mmcu=atmega16 on the command line leads to __AVR_ATmega16__ being defined inside the compiler (you can study the compiler source to see the table where this happens). In turn that means that it's this small bit of <avr/io/h> that is really active:

#elif defined (__AVR_ATmega16__)
#  include <avr/iom16.h>

so among all the ioXXX.h headers that the compiler has, when building for mega16 it is the iom16.h file that is used. In turn you can study that to find:

#define DDRB    _SFR_IO8(0x17)

You will also find that <avr/io.h> includes <avr/sfr_defs.h> and in that you will find:

#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

within that:

#else  /* !_SFR_ASM_COMPAT */

#ifndef __SFR_OFFSET
#  if __AVR_ARCH__ >= 100
#    define __SFR_OFFSET 0x00
#  else
#    define __SFR_OFFSET 0x20
#  endif
#endif

as C is the NOT SFR_ASM_COMPAT case then this will define __SFR_OFFSET as 0x20. In the same file you will also find:

#ifndef __ASSEMBLER__
/* These only work in C programs.  */
#include <inttypes.h>

#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

So the define of DDRB is:

#define DDRB   _MMIO_BYTE((0x17) + __SFR_OFFSET))

which in turn is:

#define DDRB   _MMIO_BYTE((0x17) + 0x20))

which in turn is:

#define DDRB   (*(volatile uint8_t *)((0x17) + 0x20)))

So:

#include <avr/io.h>

int main(void) {
    DDRB = 0x55;
}

is really

#include <avr/io.h>

int main(void) {
    (*(volatile uint8_t *)((0x17) + 0x20))) = 0x55;
}

but as Johan already showed in #3 you didn't need to manually unwind all the macro definitions because the C compiler will do all that for you:

C:\SysGCC\avr\bin>type avr.c
#include <avr/io.h>

int main(void) {
    DDRB = 0x55;
}

C:\SysGCC\avr\bin>avr-gcc -mmcu=atmega16 -Os -save-temps avr.c -o avr.elf

C:\SysGCC\avr\bin>tail avr.i


# 3 "avr.c"
int main(void) {

# 4 "avr.c" 3
   (*(volatile uint8_t *)((0x17) + 0x20))
# 4 "avr.c"
        = 0x55;
}

So there's no real mystery here. The -save-temps also gives you a .s file as well as .i and that has:

C:\SysGCC\avr\bin>type avr.s
        .file   "avr.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
        .section        .text.startup,"ax",@progbits
.global main
        .type   main, @function
main:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
        ldi r24,lo8(85)
        out 0x17,r24
        ldi r24,0
        ldi r25,0
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 5.3.0"

which is the actual Asm file generated by the C compiler and then passed to the assembler to create your code. The LDI and OUT being the direct result of:

# 4 "avr.c" 3
   (*(volatile uint8_t *)((0x17) + 0x20))
# 4 "avr.c"
        = 0x55;

The 0x17 versus 0x37 is because 0x20 is first added to the IO address (0x17) to make it a "RAM address" but the compiler (because of -Os) optimises this and recognises that "STS 0x37, n" in RAM is more efficient as "OUT 0x17, n" in IO space so the +0x20 offset that was added was removed in the optimisation.

 

As I say none of this is "hidden". It's an open source compiler and it's also happy to show you (and sometimes extraordinary levels of detail!) how it is working internally so there's no need to be "scared" of using things like "DDRB = 0x55". It sure makes a heap more sense than "(*(volatile uint8_t *)((0x17) + 0x20)) = 0x55"!!

 

You may feel you have "won" if you do use that tortuous syntax because "nothing is hidden" but pretty soon you are going to find it a lot easier to type:

ADCSRA = (1 << ADEN) | (1 << ADSC);

than the non-macro alternative.

 

If you do want an alternative though then consider this:

 

https://www.avrfreaks.net/forum/b...

 

That will let you use:

 

https://www.avrfreaks.net/comment...

 

There is nothing "hidden" at all in that. The entire mega16 is being defined as a C struct on top of the SFR space. The only "machine specific" detail in the whole thing is really:

#define USE_SFRS() volatile SFRS_t * const pSFR = (SFRS_t *)0x0020

where it hard codes the 0x0020 base address for the SFRs. In this scheme you just:

#include "Atmega16.h"

USE_SFRS();

int main(void) {
    ddrb = 0x55; // note case used ! "ddrb" not "DDRB"
}

Everything (and I mean everything) is immediately visible in Atmega16.h which is the ONLY support header involved. Again working it back this has:

#define ddrb pSFR->_DDRB.all

and that in turn refers to:

	union {
		uint8_t all; // (@ 0x37) Port B Data Direction Register
		struct {
			unsigned int b0:1;
			unsigned int b1:1;
			unsigned int b2:1;
			unsigned int b3:1;
			unsigned int b4:1;
			unsigned int b5:1;
			unsigned int b6:1;
			unsigned int b7:1;
		} bits;
	} _DDRB;

which (as the comment tells you) just happens to be at offset 0x17 from the 0x0020 base (so at 0x0037).

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

yorem_kastor wrote:
i can at least write my own header files for modules, chips by reading datasheets.

But what on earth is the point of that?

 

Surely, it is a complete waste of time to manually transcribe this information from the datasheet - especially when it has already been done for you!

 

That's not creative - that's just mechanical, laborious, slog.

 

Just re-inventing the wheel.

 

You don't need to re-create the entire header just to understand what it does - use your IDE's 'go-to-definition' facility to see how the symbols are defined.

Examine pre-processor output.

Step in the debugger or simulator.

 

Top Tips:

  1. How to properly post source code - see: https://www.avrfreaks.net/comment... - also how to properly include images/pictures
  2. "Garbage" characters on a serial terminal are (almost?) invariably due to wrong baud rate - see: https://learn.sparkfun.com/tutorials/serial-communication
  3. Wrong baud rate is usually due to not running at the speed you thought; check by blinking a LED to see if you get the speed you expected
  4. Difference between a crystal, and a crystal oscillatorhttps://www.avrfreaks.net/comment...
  5. When your question is resolved, mark the solution: https://www.avrfreaks.net/comment...
  6. Beginner's "Getting Started" tips: https://www.avrfreaks.net/comment...
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

yorem_kastor wrote:
It is just a small step to fully understand what is going on in the background. ...

I posed a few questions.  You have chosen not to address them.  There is a reason to ask each.  So I'm out.

theusch wrote:
First, define "library". Is avr-libc a "library"? Next, can't DDRB=0xff; be done without AVR-libc? Why would a chip-include file and subordinate .h be part of a "library"?

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

awneil wrote:
Surely, it is a complete waste of time to manually transcribe this information from the datasheet - especially when it has already been done for you!
See the thread I just linked to. All (and I mean ALL) the information about device registers in an AVR datasheet is collated into an XML file called .ATDF (Atmel Data File) - one for each device. So you can write an XML parser to read that file and massage all the register/bit information into some format you prefer to use. I happened to use Python for this (and then C struct format for output) because among other things Python has an XML reading library that is trivially simple to use. I would not be tempted to do this transcription of register/bit info manually as it's far too prone to error (in fact I already know of one or two errors in the XML which are supposed to be a "perfect" copy of the datasheet info!!).

 

It may be do-able but I think you will find it very hard to create a usable set of register/bit definitions that does not use at some point some #define'd macros.

 

Most people use C compilers for micros on the basis that some clever engineer has already done all the #define'ing so you don't have to but if you feel the need to cast an engine from aluminium, a frame and body from steel and some tyres from unprocessed rubber BEFORE you drive a car I can see why it might be an interesting intellectual exercise - but most people wouldn't choose to do it more than once!

 

This is why auto-code generators are so attractive (given availability of the source info) - they let you faultlessly create 100's of .h files automatically so no human errors are introduced (otherwise you could spend a LOT of time debugging faults in a micro program only to find a typo in the headers you manually created!). At least if a C compiler had such a fault in a .h file you can be pretty sure 3,741 people will already have found that same error before you hit it!

Last Edited: Mon. Jan 22, 2018 - 04:44 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I know.

 

But I certainly got the impression the OP was talking about actually doing the manual transcribing from the datasheet himself.

 

 

Top Tips:

  1. How to properly post source code - see: https://www.avrfreaks.net/comment... - also how to properly include images/pictures
  2. "Garbage" characters on a serial terminal are (almost?) invariably due to wrong baud rate - see: https://learn.sparkfun.com/tutorials/serial-communication
  3. Wrong baud rate is usually due to not running at the speed you thought; check by blinking a LED to see if you get the speed you expected
  4. Difference between a crystal, and a crystal oscillatorhttps://www.avrfreaks.net/comment...
  5. When your question is resolved, mark the solution: https://www.avrfreaks.net/comment...
  6. Beginner's "Getting Started" tips: https://www.avrfreaks.net/comment...
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'd at least try to do some at least half automated copy/paste process on this table from a datasheet then:

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

but my point is that's just pure laborious slog - there is nothing creative (OP's word) in doing that!

Top Tips:

  1. How to properly post source code - see: https://www.avrfreaks.net/comment... - also how to properly include images/pictures
  2. "Garbage" characters on a serial terminal are (almost?) invariably due to wrong baud rate - see: https://learn.sparkfun.com/tutorials/serial-communication
  3. Wrong baud rate is usually due to not running at the speed you thought; check by blinking a LED to see if you get the speed you expected
  4. Difference between a crystal, and a crystal oscillatorhttps://www.avrfreaks.net/comment...
  5. When your question is resolved, mark the solution: https://www.avrfreaks.net/comment...
  6. Beginner's "Getting Started" tips: https://www.avrfreaks.net/comment...
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Atmel Data File

avr-tools-device-file actually (just nitpicking...)

:: Morten

 

(yes, I work for Atmel, yes, I do this in my spare time, now stop sending PMs)