The Pico In MicroPython: Direct To The Hardware |
Written by Harry Fairhead & Mike James | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Monday, 13 December 2021 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Page 2 of 2
Single-Cycle IO BlockAt this point you might think that we are ready to access the state of the GPIO lines for general input and output. This isn’t quite the whole story. To accommodate the fact that the processor has two cores, and to make access faster to important devices, there is a special connection, the SIO or Single-cycle IO Block, between the cores and, among other things, the GPIO. The SIO connects directly to the two cores and they can perform single-cycle 32‑bit reads and writes to any register in the SIO. Notice that the SIO is not connected via the general address bus. You can see the general structure of the SIO in the diagram below. You can find out about the other devices it connects to from the documentation - our focus is on the GPIO lines.
Notice that the GPIO lines are multipurpose and the SIO only has control when they are being used as GPIO lines. In this sense the SIO is just another peripheral that can take control of a GPIO line. The SIO provides a set of registers that makes using the GPIO much faster and much easier. The basic registers are: GPIO_OUT Sets all GPIO lines to high or low GPIO_IN Reads all GPIO lines GPIO_OE Sets any GPIO line to output driver or high impedance There are also three registers – SET, CLR and XOR - that make working with GPIO_OUT and GPIO_OE easier. Each of these can be thought of as a mask that sets, clears or XORs bits in the corresponding register. For example, GPIO_OUT_SET can be used to set just those bits in GPIO_OUT that correspond to the positions that are set high. The locations of these registers are as offsets from 0xd0000000:
Blinky RevisitedNow we can re-write Blinky yet again, but this time using direct access to the SIO GPIO registers. from machine import mem32,Pin from time import sleep_ms led=Pin(25,mode=Pin.OUT) addrSIO = 0xd0000000 while True: mem32[addrSIO + 0x014] = 1 << 25 sleep_ms(500) mem32[addrSIO + 0x018] = 1 << 25 sleep_ms(500) This program uses the standard MicroPython class to set the GPIO line to SIO control and output. If you think that this is cheating, it is an exercise to set the line correctly using the GPIO control register and the SIO. This example is a demonstration rather than being useful, but there are some very useful functions we can write using our knowledge of how the GPIO lines are controlled. MicroPython is limited to controlling a single GPIO line at a time, but the hardware can change or read multiple GPIO lines in a single register operation. For example: def gpio_get():
return mem32[0xd0000000+0x010]
Here the get function simply reads the GPIO_OUT register which has a single bit for the output state of each GPIO line. Notice that GPIO lines set to output reflect their last written-to state. A set function simply writes the mask to the GPIO_OUT_SET register def gpio_set(mask):
mem32[0xd0000000+0x014] = mask
A clear function is just as easy and this just writes to the GPIO_OUT_CLR register: def gpio_clear(mask):
mem32[0xd0000000+0x18C] = mask
You can easily create functions for reset and other logical operations on all of the GPIO lines in one operation, but a single mask value function is usually sufficient: def gpio_set(value,mask):
mem32[0xd0000000+0x01C] =
This writes to the GPIO_OUT_XOR register, but it writes a combination of a mask and a value. The mask gives the GPIO lines that need to be changed and the value gives the state they are to be set to. For example, if mask is 0111 and value is 0100 then value & mask is 0100. If this is XORed with the current state of the lines – e.g. 0101, in this case the result is 0001, which changes the state of only GP0 to a zero. Thus we have set lines GP2, GP1 and GP0 as specified in the mask to the corresponding bits in the value, i.e. 0100. Notice that this process sets the lines selected in the mask to either a zero or a one as determined by the bits in value. As demonstrated in Chapter 4, the value,mask function can be used to set GPIO lines simultaneously: from machine import Pin import machine def gpio_get(): return machine.mem32[0xd0000000+0x010] def gpio_set(value,mask): machine.mem32[0xd0000000+0x01C]= This sets lines GP21 and GP22 to 01 and 10 on each pass through the loop: In Chapter But Not In This Extract
Summary
Programming the Raspberry Pi Pico/W In MicroPython Second EditionBy Harry Fairhead & Mike JamesBuy from Amazon. Contents
Also of interest: <ASIN:1871962803> <ASIN:B0BR8LWYMZ> <ASIN:187196279X> <ASIN:B0BL1HS3QD> To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.
Comments
or email your comment to: comments@i-programmer.info Chapter 18Direct To The HardwareMicroPython provides classes and methods to let you access most of the major hardware features of the Pico. They are very simple wrappers around the basic mechanism of working with the hardware – memory-mapped registers. Unfortunately at the time of writing there are many hardware features which are simply not exposed via MicroPython. In most cases it is possible to extend what you access using lower-level interactions with the hardware – still staying in MicroPython, but writing and reading the low-level register based hardware. The obvious reason for knowing how to use memory-mapped registers is that if MicroPython doesn’t provide a function that does what you want, create it! Perhaps a better reason is just to know how things work. In this chapter we take a look at how the Pico presents its hardware for you to use and how to access it via basic software. RegistersSome processors have special ways of connecting devices, but the Pico’s processor uses the more common memory-mapping approach. In this, each external device is represented by a set of memory locations or “registers” that control it. Each bit in the register controls some aspect of the way the device behaves. Groups of bits also can be interpreted as short integers which set operating values or modes. How do you access a register? MicroPython provides a number of ways of doing this but the simplest is to make use of the mem functions in the machine module: machine.mem32[address] Returns or sets a 32-bit value at the address machine.mem16[address] Returns or sets a 16-bit value at the address machine.mem8[address] Returns or sets an 8-bit value at the address
The only difficult part is in working out the address you need to use and the value that sets or resets the bits you need to modify. For example, if you look in the documentation you will find that the GPIO registers start at address 0x40014000. The registers are defined by their offset from this starting address. So for example, the table of GPIO registers is:
You can see that there are two registers for each GPIO line from GP0 to GP29, one control register and one status register. Each register has the same format for each GPIO line. For example, the status register is:
You can see that many of the 32 bits in the register are not used, but bit 9 is OUTTOPAD which is the final state of the GPIO line after register overrides have been applied. You can read its current value using: from machine import mem32 addrGP0Status= 0x40014000 value=mem32[addrGP0Status] print(bin(value)) This prints the current status of GP0 in binary. If you want to find the status of GPn you need to use address 0x40014000+2n. Usually addresses are specified as a base address, i.e. where things start, and an offset that has to be added to the base to get the address of a specific device. This is the general way you work with peripheral devices such as the PWM units or I2C hardware, but the GPIO is special in that it has another set of registers that control it. Single-Cycle IO BlockAt this point you might think that we are ready to access the state of the GPIO lines for general input and output. This isn’t quite the whole story. To accommodate the fact that the processor has two cores, and to make access faster to important devices, there is a special connection, the SIO or Single-cycle IO Block, between the cores and, among other things, the GPIO. The SIO connects directly to the two cores and they can perform single-cycle 32‑bit reads and writes to any register in the SIO. Notice that the SIO is not connected via the general address bus. Yo Notice that the GPIO lines are multipurpose and the SIO only has control when they are being used as GPIO lines. In this sense the SIO is just another peripheral that can take control of a GPIO line. The SIO provides a set of registers that makes using the GPIO much faster and much easier. The basic registers are: GPIO_OUT Sets all GPIO lines to high or low GPIO_IN Reads all GPIO lines GPIO_OE Sets any GPIO line to output driver or high impedance There are also three registers – SET, CLR and XOR - that make working with GPIO_OUT and GPIO_OE easier. Each of these can be thought of as a mask that sets, clears or XORs bits in the corresponding register. For example, GPIO_OUT_SET can be used to set just those bits in GPIO_OUT that correspond to the positions that are set high. The locations of these registers are as offsets from 0xd0000000:
Blinky RevisitedNow we can re-write Blinky yet again, but this time using direct access to the SIO GPIO registers. from machine import mem32,Pin from time import sleep_ms led=Pin(25,mode=Pin.OUT) addrSIO = 0xd0000000 while True: mem32[addrSIO + 0x014] = 1 << 25 sleep_ms(500) mem32[addrSIO + 0x018] = 1 << 25 sleep_ms(500) This program uses the standard MicroPython class to set the GPIO line to SIO control and output. If you think that this is cheating, it is an exercise to set the line correctly using the GPIO control register and the SIO. This example is a demonstration rather than being useful, but there are some very useful functions we can write using our knowledge of how the GPIO lines are controlled. MicroPython is limited to controlling a single GPIO line at a time, but the hardware can change or read multiple GPIO lines in a single register operation. For example: def gpio_get():
return mem32[0xd0000000+0x010]
Here the get function simply reads the GPIO_OUT register which has a single bit for the output state of each GPIO line. Notice that GPIO lines set to output reflect their last written-to state. A set function simply writes the mask to the GPIO_OUT_SET register def gpio_set(mask):
mem32[0xd0000000+0x014] = mask
A clear function is just as easy and this just writes to the GPIO_OUT_CLR register: def gpio_clear(mask):
mem32[0xd0000000+0x18C] = mask
You can easily create functions for reset and other logical operations on all of the GPIO lines in one operation, but a single mask value function is usually sufficient: def gpio_set(value,mask):
mem32[0xd0000000+0x01C] = (machine.mem32[0xd0000000+0x010])^value & mask
This writes to the GPIO_OUT_XOR register, but it writes a combination of a mask and a value. The mask gives the GPIO lines that need to be changed and the value gives the state they are to be set to. For example, if mask is 0111 and value is 0100 then value & mask is 0100. If this is XORed with the current state of the lines – e.g. 0101, in this case the result is 0001, which changes the state of only GP0 to a zero. Thus we have set lines GP2, GP1 and GP0 as specified in the mask to the corresponding bits in the value, i.e. 0100. Notice that this process sets the lines selected in the mask to either a zero or a one as determined by the bits in value.
As demonstrated in Chapter 4, the value,mask function can be used to set GPIO lines simultaneously: from machine import Pin import machine def gpio_get(): return machine.mem32[0xd0000000+0x010] def gpio_set(value,mask): This sets lines GP21 and GP22 to 01 and 10 on each pass through the loop:
Example I - EventsIn Chapter 7 the idea of events was introduced, but MicroPython doesn’t provide any access to events. The solution is to add our own function that accesses the GPIO register that records interrupts. This is a register in the GPIO set of registers rather than in the SIO as GPIO interrupts aren’t specific to what is controlling the GPIO line. The base address of the GPIO registers is 0x40014000. After the end of the set of status and control registers, starting at offset 0x0f0, there is a block of four interrupt registers that record the status of the GPIO lines. Each group of four bits gives the status of the various level and edge events:
The format of the raw interrupt registers is more complicated than the previous one bit to one GPIO line arrangement we have encountered before. In this case there are four bits per GPIO line and they record different interrupt types. The first four bits of the first register record interrupts on GP0:
This pattern is repeated for each of the GPIO lines and, when all of the bits in the first register have been used, the pattern continues in the second register with GP8 and so on. Each register records the event data for eight GPIO lines. Notice that each of the bits is set if the event that would cause the interrupt occurs – the interrupt itself only occurs if it is enabled. What this means is that the level bits track the current level of the GPIO line and the edge bits are set if an edge of that type has occurred. The WC in the third column indicates that the bit is cleared if you write to it and this is how the event is cleared. The new problem here is that we have to work out which register is concerned with which GPIO line – each register looks after eight GPIO lines – and which group of four bits in the register gives the events for that GPIO line. The following function accepts the GPIO number and works out which register and group of bits it corresponds to: def gpio_get_events(pinNo): mask = 0xF << 4 * (pinNo % 8) intrAddr = 0x40014000 + 0x0f0 + (pinNo // 8)*4 return (machine.mem32[intrAddr] & mask) >> (4 * (pinNo % 8)) The calculation for the address of the register needed is just the base address plus the offset of the first interrupt register and then +(pinNo//8)*4. As each register deals with a group of 8, pinNo//8 (integer division) gives the number of the register needed and *4 converts this to a byte address, i.e. each register is four bytes. The mask is constructed using a similar technique. The first GPIO line uses four bits starting at 0, the second uses four bits starting at 4 the third at 8 and so on, i.e. (4*(pinNo % 8), and this is used to create a mask. Once you have the basic way of accessing the bits you need, you can reuse it to create a clear events function: def gpio_clear_events(pinNo, events): intrAddr = 0x40014000 + 0x0f0 + (pinNo // 8)*4 machine.mem32[intrAddr] = events << (4 * (pinNo % 8)) To clear the raw interrupt bit you simply have to write a zero to it. Example II PAD - Pull, Drive and SchmittEach GPIO line has an identical input output stage, called a PAD, which is the connection to the outside world, no matter what mode the GPIO line is being used in. This is fundamental to the workings of the GPIO line and you might be wondering why it is being introduced so late? The answer is that MicroPython doesn’t fully support it and the aspects of the PAD that it does support, Pull Up and Pull Down, are simple enough. If you are interested in the finer details then you are going to have to implement functions that work with them. The structure of the PAD can be seen below: You can see that under program control you can set the pull up/pull down configuration and enable the input/output. The input also has a Schmitt trigger that can be enabled to clean up noisy inputs. The output can be customized by slew rate, how fast it changes and drive strength. Before moving on to the software, it is worth explaining the basic ideas of the options. The Schmitt trigger adds hysteresis to the input line. This means that before the state changes from high to low it has to cross a threshold, but to change back to a low state it has to cross a lower threshold. This acts like a limited debounce mechanism in that it stops the line from going low because the input drops a little after going high. The Pico’s Schmitt trigger uses thresholds of a 0.2V difference if the processor supply voltage is 2.5V to 3.3V and 0.18V if the voltage is 1.8V. What this means is that at 3.3V the input has to be greater than 2V to be a one, but after this the voltage has to fall to 1.8V before it changes back to a zero. For a zero, the thresholds are 0.8V to change a zero and 2V to change back to a one. The output drive strength isn’t to do with how much current the GPIO line can source, it is about the voltage output at different currents. It is the effective output resistance. Each time the drive current is increased by 2mA another transistor is used in the drive, so lowering the output resistance. This has the effect of increasing or decreasing the voltage at the pin. For example, if you set the drive to 1 then, if you want to keep the output voltage at or above 2.62V, i.e. a logic 1, you can’t draw more than 2mA. Put more simply, if you want the output to maintain voltages that are within the threshold for a logic 1 or 0 then you can only draw 2, 4, 8 or 12mA from the GPIO line depending on the setting of the drive strength. If you draw more current, this is fine, but the voltage will fall below the standards for 3.3V logic. The final option is to change the slew rate. There is no information on this in the datasheet, but slowing the rate of change of the GPIO line can be useful when driving long lines or reactive loads. How useful this is in practice is a matter of experimentation. If you want to work with the PAD directly then you need to know the location and format of the PAD control registers. These start at 0x4001c000 and the first, controlling the GP0 PAD is at offset 0x04 and in general the register for GPn PAD is at offset 4(n+1). The format of the PAD register is:
Using this information it is fairly easy to write functions to set each of the characteristics of the PAD. For example, to read the slew rate: def pad_get_slew(gpio):
return mem32[0x4001c000+(gpio+1)*4] & 0x01
To set the skew rate: def pad_set_slew(gpio,value): if value: mem32[0x4001c000+(gpio+1)*4]= Getting and setting the Schmitt trigger is just as easy, but now we set or clear the second bit not the first: def pad_set_schmitt(gpio,value): if value: mem32[0x4001c000+(gpio+1)*4]= Setting the drive is slightly more complicated as it is a three-bit value: def pad_get_drive(gpio): return (mem32[0x4001c000+(gpio+1)*4] & 0xE0) >> 5 def pad_set_drive(gpio, value): mem32[0x4001c000+(gpio+1)*4]= The set function uses the same logic as the value mask function given earlier to only change the bits we are interesting in changing. Of course we, don’t need to deal with the pull-ups or pull-downs as there are methods that do the job. Digging DeeperThere is so much more to explore, but now we have covered the details that make things difficult to get started. From here you can read the datasheet to find out how the registers control things and implement MicroPython functions to extend what you can do. The biggest difficult is finding the register that contains the bits that reflect the status or control whatever it is you are interested in. Once you have found this out, the only remaining difficulty is in working out how to set or clear the bits you need to work with without changing other bits. It also has to be said that hardware documentation at this level is often incomplete due to assumptions the writer makes about what you should already know. In such a circumstance your best approach is the experimental method. Work out the simplest program you can think of to verify that you understand what the hardware does – and if you are wrong always check the addresses and bits you are changing before concluding that things work differently to the manual.
Summary
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Last Updated ( Monday, 13 December 2021 ) |