Non-destructive Bit Setting in Mecrisp Stellaris Forth
Doing pretty much anything involving a peripheral on the STM32F103 microcontroller (or on most microcontrollers for that matter) involves putting ones or zeros into registers or reading the registers to see which bits are ones or zeros. The simplest way to do this is to write or read the number you want to write into the register. In Forth, this is done using the !
and @
words, respectively.
For example, $33333333 $40011000 !
stores the appropriate bit pattern to turn all the lower 8 pins of the general-purpose input-output port C (GPIOC) to be 50 MHz push-pull outputs, by setting all the bits of the GPIOC_CRL register located at address $40011000.
While this is easy to do, it has the disadvantage that whatever used to be in that register has now been obliterated by the $33333333
. Often we just want to change the state of one pin of one of our GPIO ports without interfering with the others. This is particularly true of ports that operate more than 1 peripheral… you don’t want to lose your serial connection when changing a GPIO port.
So what we need is the ability to set 1 or more bits in a register without changing any of the others. An example of this sort of requirement is the setting of the GPIOx_CRH or GPIOx_CRL register to determine whether a particular pin in a GPIO port is an input or an output. This is done by setting 4 bits in this register. Which particular 4 bits to set, and indeed which of the CRL or CRH registers to set them in, will be different depending on the pin you want to set and the port containing that pin.
As an example, say that we want to set pin 10 of GPIO port C to be a push-pull output operating with a clock speed of 50 MHz. As it’s higher than pin 7, the appropriate register to set is GPIOC_CRH, located at $40011004 on the STM32F103 chip. The diagrammatic representation of this register, based upon the chip manual, is shown in the figure below
Each of pins 8 to 15 of the particular port (in this case port C) is controlled using 4 bits. The lower 2 bits are called mode bits and determine whether the port is an input or an output, and what speed it outputs to if it is the latter type. The upper two bits provide further characteristics of the input/output port. The lowest 4 bits control pin 8, the next higher 4 bits control pin 9 and so on.
For this case, if we want to set pin 10 to be a push-pull output at 50 MHz clock rate, we need to set bits 8-11 of this register to the binary number %0011. But we want to achieve this without changing the values of any of the other pins in this half of the port. The desired change in the port is shown below.
The x’s indicate either 0 or 1 values in that bit position.
Bitwise Logic
So if we want to set just these bits then we have to recall some bitwise Boolean logic operations. Specifically we will recall three important bitwise operations: NOT, AND and OR.
NOT is the simplest of these, as it only operates on single bits, reversing their value at every bit position. Thus, 0s become 1s and 1s become 0s. The truth table for the NOT operation looks like this:
NOT
Input | Output |
---|---|
0 | 1 |
1 | 0 |
AND and OR both operate on two numbers, one bit at a time.
AND
Input 1 | Input 2 | Output |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
OR
Input 1 | Input 2 | Output |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
In the case of AND, the only time a bit is not set to 0 is when both input bits are 1. Similarly, for OR, the only time a bit is not set to 1 is when both input bits are 0.
We can also use the XOR operation to toggle bits where that is needed:
XOR
Input 1 | Input 2 | Output |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Looking at the truth table for AND, you can see that *AND*ing a bit with 0 always sets it to 0, while *AND*ing a bit with 1 leaves the original bit unchanged. Similarly, ORing a bit with 1 always sets it to 1 while *OR*ing a bit with 0 leaves the original bit unchanged.
Thus, if I want to set a bit within a number to 1 while not changing the value of any of the other bits, I need to have a number of the same length with 0s everywhere except at the position that I need to set to 1.
To set a bit to 0, I need the opposite. I need to AND each bit with a 1 where I want to leave the bit unchanged and to AND with a 0 at the location of the bit I want to reset to 0. To do this conveniently, one typically puts a 1 at the location to reset, performs the NOT operation to all the bits, turning that bit to 0 and all the other bits in the number to 1s, then AND with the original value. By doing this, all the other bits are unchanged because they are being *AND*ed with 1 and the bit you wish to reset is always 0 regardless of whether it was a 0 or a 1 because you are *AND*ing that bit with 0. This takes a little concentration to get one’s head around, but it works.
Shifting a Bit Pattern in Forth
To get a particular pattern in the correct place we can use the Forth word LSHIFT ( pattern nshift -- )
. A phrase like 1 10 LSHIFT
would put a 1 on the stack and shift it to bit position 10, with 0s at every other bit position. Then *OR*ing the register value with this number would ensure that there was a 1 at position 10 and 0s everywhere else. This is basically how a word is set. This can also be done for more than one 1. For example to set pins 10 and 11 to a 1, you would OR with a number made using %11 10 LSHIFT
. Note that I’m assuming the Forth is in DECIMAL here. Any pattern of bits can be shifted using LSHIFT. The word always puts 0s in the new bit positions made by moving the bits to the left, and the bits on the right that are shifted out are lost.
Resetting a bit is a little more complicated. The first step is the same as for setting. Put a 1 at the position you want to change by using LSHIFT. However, now instead of using OR, you must use NOT to invert each bit so you have a zero at the reset bit positions and 1s everywhere else and then use AND to reset only those bits with a 0 at their location.
Setting and Resetting Bit Patterns
While setting or resetting single bits or strings of the same bits is not so complicated, there are a couple of extra complications when one wishes to use the same method to set a series of 0s and 1s. The first is that leading zeros are significant, unlike for numbers. The second is that the non-destructive replacement of bits needs to happen in two stages, as we will now illustrate.
The sequence of operations required to non-destructively set some bits in a register is shown in the diagram below:
Step 1 involves resetting all the bits occupied by the bit pattern to 0. If we have 4 bits that we need to set/reset, we shift 4 1s into the appropriate bit position using LSHIFT
(in this case %1111 8 LSHIFT
), then NOT the number to make those 4 bits zeros and all other bits 1s, then finally AND with the value in the register. This sets the 4 bits we want to modify to zeros while leaving the other bits unchanged, as shown in the first register diagram. After setting those bits to 0s, we put our bit pattern on the stack, shift it by the same number of places and then OR with the number currently in the register. After this second process, our bit pattern (%0011) is located in bits 8 through 11, while the other bits remain unchanged.
As we stated before, the zeros in a number like %0011 are significant in this case, because they contain information about the number of bits that need to be replaced. This causes difficulties with just using the stack to store these numbers because the leading zeros are lost. This means we will need to keep a separate record of the number of bits to mask out with our zeros in the first step.
The Forth Routines
Now that we know what to do, we just need to code it up. Firstly, a word that puts a given number of 1s on the stack, to be used and shifted to clear the bits we want to set. Given a number on the stack, we can generate 1s in a loop, noting that each binary place is generated by multiplying by 2. Thus we can multiply by 2 and add 1 in a loop to generate our string of 1s.
: ones ( n -- %11..1 ) \ Generate a binary number consisting of n 1s 1 SWAP 1- 0 ?DO 2 * 1 + LOOP ;
The next word works out how many places we need to shift our bit pattern to get it to the right starting point. This involves a design decision… do we work in terms of bit positions to shift, or in terms of the bit position of the most significant bit. These two are only the same when shifting single bits: otherwise the number of places to shift varies with the length of the binary string to be shifted. As I find it easier to operate in terms of the bit position of the most significant bit (MSB), we need a word that will use that information to determine the number of places to shift the bits. Note that to determine this, we need to keep track of the number of bits we are shifting (see previous discussion on significant bits). As such, we keep the number of bits as a separate number on the stack
: pos_shift ( nbits pos -- nbits shift# ) \ Determines the number of bits to shift given the position of the MSB \ and the number of bits OVER - 1+ ;
Now we are able to make our bit-clearing mask, which I have called not_mask
.
: not_mask ( nbits shift -- shift mask ) \ Generate mask consisting of 1s everywhere but where we want to \ change bits SWAP ones OVER LSHIFT NOT ;
Our final utility word for setting/resetting a given bit pattern is called set_bits
. This takes 4 arguments on the stack: addr
%n
nbits
and pos
. addr
is the address of the register, %n
is the binary bit pattern, nbits
is an integer indicating the number of bits in the pattern and pos
shows the location of the MSB of the number in the bit pattern.
: set_bits ( addr %n nbits pos -- ) \ Stores a bit pattern bits starting at a given bit position at address adr \ bits consists of nbits 1s and 0s at position pos in a 32-bit word. \ Non-intrusive for all other bits. \ Usage: \ GPIOC CRH %0011 4 7 set_bits \ This would place the 4-bit pattern %0011 at bit position 7 in GPIOC_CRH. \ The word b counts the bits (including leading zeros) in the binary number. \ Note that b can only be used interactively, not within a word definition. pos_shift \ Determine number of bits to shift pattern not_mask \ Set bit pattern to AND with >R LSHIFT \ Set bit pattern to OR with OVER @ R> AND \ AND with mask to get 0s at correct bit positions OR \ OR with bit pattern to nonintrusively set SWAP ! ; \ Store new bit pattern at address
In this case, calling $40011004 %0011 4 11 set_bits
would put the bit pattern %0011 in the place in GPIOC_CRH that sets pin 10 to be an output.
This routine is flexible enough to use with bit patterns of any length at any position.
In a later blog entry we will use these routines to generate a very general wordset for controlling the GPIO ports on the STM32F103.
Comments
Hi Sean,
I discovered your articles and I am glad you like my Forth! I am biophysicist and I wrote Mecrisp-Stellaris for signal capture and experiment control while doing research for my doctoral thesis in resonance Raman spectroscopy on the violaxanthin cycle kinetics in photosynthesis.
Best wishes from the author of Mecrisp-Stellaris,
Matthias
Thanks Matthias. It’s great to have you as my first non-spam and non-me commenter. I have written separately to you, but I’d like to thank you for providing Mecrisp Stellaris to the community. And it’s great to know it was made to do science with!