NXP LPC1112 Cortex-M0 I/O

28 December 2018
A while ago I obtained some ARM Cortex-M0 chips and got both a development setup and a very basic firmware working, but since then had on my to-do list finding out how to get various input/output sub-systems working so that the chip would be of practical use: simple pin read/writes, RS232 communications, and I2C interfacing. This article covers these topics, complete with references to sample ARM code within my Bitbucket repository — my focus is on minimalist from-scratch samples so there are no dependencies beyond the hardware and tool-chain.

GPIO test circuit

While my first Cortex-M0 firmware was functional, there were faults that only became apparent when writing more sophisticated firmware, such as not zeroing out memory within the .bss code segment. I also wanted to seperate the boiler-plate interrupt handlers into a chip-specific stub, leaving a simplified API for me to build firmware programs on top of — I have no doubts that I will end up extending this stub to allow for more functonality. A notable change in the process was rigging the linker script so that the checksum value that the LPC1112 expects is not affected by code outside of the stub.

Building a more recent ARM GCC

Unlike the other Slackware tool-chain components the SlackBuild for ARM GCC uses GCC v4.9.2 which is rather dated, but it is pretty easy to use the same script to build the more recent v7.4.0 instead — the relevant commands are listed below. The patch gcc7arm.patch changes VERSION=${VERSION:-4.9.2} to VERSION=${VERSION:-7.4.0} so that the script uses the correct version number, and gcc-$VERSION.tar.bz2 is changed to gcc-$VERSION.tar.gz since GCC no longer ships in BZip2 archives.

wget https://slackbuilds.org/slackbuilds/14.2/development/arm-gcc.tar.gz tar -xzvf arm-gcc.tar.gz patch -p0 < gcc7arm.patch cd arm-gcc wget ftp://ftp.gnu.org/pub/gnu/gcc/gcc-7.4.0/gcc-7.4.0.tar.gz ./arm-gcc.SlackBuild installpkg /tmp/arm-gcc-7.4.0-x86_64-1_SBo.tgz

Note that the last two commands have to be run as root. See my previous article for other tool-chain details

Basic pin configuration

Within the LPC111x user manual the section that covers pin configuration is 37 pages long, and this section distills down the information relevant to the pins that appear on the 20-pin LPC1112. Although each pin is configured using a 32-bit register only the bottom eleven are used, and the purpose of each bit is summarised in the table below with defaults highlighted in bold. The I2C-capable pins (5 and 20) have an on-reset default of 0x00 whereas all the other pins have a reset value of 0xd0. Note that for ports not capable of analogue input bit 7 has no effect, but the chipset specs adopt the convention of having it default to set (i.e. high) for digital-only ports anyway. For some reason Bit 6 also defaults to high even though no pin uses it.

Bits
012345 678910 11-31
Pins 1-3
Pins 5-6
Pins 11-12
Pins 17-20
Function Mode Hysteresis reserved Analog/Digital reserved Output reserved
See
Pin Functions
0x0 Inactive
0x1 Pull-down
0x2 Pull-up
0x3 Repeater mode
0x0 Off
0x1 On
0x0 Analog
0x1 Digital
0x0 Normal GPIO
0x1 Pseudo-drain
Pin 4
Pins 7-10
reserved
Pin 5
Pin 20
Function reserved I2C Mode reserved
0x0 GPIO
0x1 I2C
0x0 Standard/Fast I2C
0x1 Standard I/O
0x2 Fast+ I2C

Configuration notes

Mode
Mode is whether internal pull-up and pull-down resistors are enabled. When mode is inactive it means input is neither pulled up nor pulled down, and repeater mode uses feedback so that if the input becomes floating the input port retains the previous driven value. Mode should be zero for analog input.
Hysteresis
When enabled hysteresis uses different voltage thresholds for low-to-high and high-to-low transitions, which is meant to avoid toggling in the presence of line noise. I suspect it is implemented using a Schmitt trigger. Hysteresis must be disabled when the power supply is less than under 2.5 volts.
Output
Pseudo-drain output mode is where the pin is either pulled to zero or left floating. It is implemented using a strong pull-down rather than a transistor open-drain.
I2C pins
The I2C-capable pins are a bit different in that when they are configured as GPIO, they are open-drain rather than driven via strong pull-up & pull-down. Standard/Fast I2C includes a glitch filter and is intended for I2C clock speeds upto 400kHz, whereas Fast+ I2C uses a high-current sink for clock speeds of 400kHz-1MHz. Fast+ I2C should not be selected when pin is being used for GPIO. The purpose of Standard I/O is not made clear in the chipset documentation.

Pin functions

The functions available on each pin are shown in the table below, which is sorted by ascending configuration register address rather than pin number. Although the configuration registers allow a pin to have seven different functions, all pins only have between two and four different functions, and in all cases GPIO is either 0x0 or 0x1.

Address Pin GPIO Port Function 0x00 (default) Function 0x01 Function 0x02 Function 0x03
0x4004400c 17 Port 0 Bit 0 Reset (active low) GPIO reserved
0x40044010 18 Port 0 Bit 1 GPIO ClkOut CT32B0_MAT2 reserved
0x4004401c 19 Port 0 Bit 2 SSEL0 CT16B0_CAP0
0x40044030 20 Port 0 Bit 4 GPIO (open drain) I2C SCL reserved
0x40044034 5 Port 0 Bit 5 I2C SDA
0x4004404c 6 Port 0 Bit 6 GPIO reserved SCK0 reserved
0x40044060 1 Port 0 Bit 8 MISO0 CT16B0_MAT0
0x40044064 2 Port 0 Bit 9 MOSI0 CT16B0_MAT1
0x40044068 3 Port 0 Bit 10 SWCLK (debugger clock) GPIO SCK0 CT16B0_MAT2
0x40044074 4 Port 0 Bit 11 reserved Analog input CT32B0_MAT3
0x40044078 7 Port 1 Bit 0 CT32B1_CAP0
0x4004407c 8 Port 1 Bit 1 CT32B1_MAT0
0x40044080 9 Port 1 Bit 2 CT32B1_MAT1
0x40044090 10 Port 1 Bit 3 SWDIO (debugger I/O) CB32B1_MAT2
0x400440a4 11 Port 1 Bit 6 GPIO RXD CT32B0_MAT0 reserved
0x400440a8 12 Port 1 Bit 7 TXD CT32B0_MAT1
n/a 13 n/a XTALIN (input to internal clocking circuits)
14 XTALOUT (oscillator amplifier out)
15 Vcc power connection
16 Ground power connection

General Purpose Input/Output

My natural inclination would be to leave low-level bit-banging to PIC Microcontrollers and use ARM for higher-level processing, but since GPIO is the most basic way to test that a chip is actually working, configuring pins for this functionality is presented here. Since there is not that much to GPIO anyway — the relevant chapter within the user guide is only eight pages long — I decided to make this section complete as well. GPIO-specific functionality is accessed through the registers shown in the table below — with all GPIO registers the bottom twelve bits (i.e. bits 0 thru 11 inclusive) correspond to the pins within a GPIO port, although on the LPC1112 not all of these pins will be used.

Address Description
Port 0 Port 1 Name Effect when Low Effect when High
0x50003ffc 0x50013ffc Data read/write See GPIO reads & writes
0x50008000 0x50018000 Direction Input pin Output pin
0x50008004 0x50018004 Interrupt Sense Edge trigger Level trigger
0x50008008 0x50018008 Interrupt Both Edges See Interrupt Event Both edges trigger
0x5000800c 0x5001800c Interrupt Event Trigger on falling/low Trigger on rising/high
0x50008010 0x50018010 Interrupt Mask Interrupt masked out Interrupt enabled
0x50008014 0x50018014 Raw Interrupt State No interrupt Interrupt occurred
0x50008018 0x50018018 Masked Interrupt State No (or masked out) interrupt Interrupt occurred
0x5000801c 0x5001801c Interrupt Clear No effect Clear edge detection

By default GPIO pins are input with built-in pull-up enabled. I am not sure if setting the corresponding bits of both interrupt sense and interrupt both edges is valid. Note that if a floating pin is switched from output to input the value it returns upon a subsequent read will be unpredictable. Ports 2 and 3 would use addresses in the ranges 0x5002xxxx and 0x5003xxxx respectively if they were present

GPIO interrupts

I have tended to prefer polling rather than interrupts when writing control firmware, but GPIO shows quite nicely how interrupts operate on the LPC1112. Edge-triggered interrupts remain asserted until reset using the 0x500x801c register, whereas level-sensitive interrupts remain asserted until the level changes away from the trigger level. A consequence of the latter is that the interrupt handler will be continually called unless the pin is masked out. The difference between the raw and masked interrupts is that only interrupts that are not masked out will cause the interrupt handler to be called. My LPC1112 GPIO demo uses edge-triggered interrupts to implement an LED toggle.

GPIO reads and writes

For a read or write the lower bits of the address are a bit-mask — for a read the returned value will be the logical-and of the pin states and this bit-mask, and for a write the set bits within the bit-mask indicate which pins will be set rather than left unchanged. There is a gotcha though, and that is the bit-mask occupies bits 2 thru 13 rather than the bottom twelve bits, so the bit-mask is effectively shifted up two bits — this is because addresses in ARM code have to be a multiple of four, so the bottom two bits cannot form part of the mask. Some of the mappings between address and bits affected are shown in the table below:

Address Bits affected
Port 0 Port 1 012345 67891011
0x50000000 0x50010000
0x50000004 0x50010004
0x50000008 0x50010008
0x50000010 0x50010010
0x50000020 0x50010020
0x50000040 0x50010040
0x50000080 0x50010080
0x50000100 0x50010100
0x50000200 0x50010200
0x50000400 0x50010400
0x50000800 0x50010800
0x50001000 0x50011000
0x50002000 0x50012000
0x50003ddc -
- 0x5001033c
0x50003ffc 0x50013ffc

The address nibbles 0x3ddc and 0x033c mask out the pins that are not physically present on ports 0 and 1 respectively. Reading of a pin that is not setup as a GPIO still returns the pin’s actual state, which may well be random — hence why it is worth masking out non-GPIO inputs during reads.

Attaching a debugger

The GPIO demo is a good choice for showing the attachment of a debugger, and a simple debugging session is presented below to show the basic commands — highlighed in bold — that are specific to debugging firmware rather than desktop applications. The OpenOCD documentation covers using GDB, so consult the latter for details. One major pit-fall is that Windows GDB does not properly handle Ctrl-C, so you may want to use a Linux box for firmware development — at time of writing I was overseas and hence relegated to using a Windows10 laptop.

$ arm-none-eabi-gdb gpio.elf Output omitted (gdb) target remote localhost:3333 Remote debugging using localhost:3333 ARMmain () at gpio.c:26 26 while(1); (gdb) monitor reset halt target halted due to debug-request, current mode: Thread xPSR: 0xc1000000 pc: 0x1fff0040 msp: 0x10000ffc (gdb) load Loading section .text, size 0x288 lma 0x0 Start address 0x0, load size 648 Transfer rate: 82 bytes/sec, 648 bytes/write. (gdb) b gpio.c:35 Breakpoint 1 at 0x222: file gpio.c, line 35. (gdb) c Continuing. Note: automatically using hardware breakpoints for read-only addresses. Breakpoint 1, ARMirq (value=31) at gpio.c:35 35 if(toggle) (gdb) p &toggle $1 = (uint32_t *) 0x10000000 <toggle> (gdb) p toggle $2 = 0 (gdb) s 42 REG(0x50003018) = 0x04; (gdb) s 43 toggle = 1; (gdb) s 45 REG(0x5000801c) = 0x100; // Clear interrupt (gdb) s 46 } (gdb) x Continuing.

The load command is not required if you have already flashed the firmware to the chip, but is included for completeness. Details on how to use gdb once attached to the firmware is beyond the scope of this article. If you get the following error regarding the target not being halted when using the target remote localhost:3333 command:

Info : accepting 'gdb' connection on tcp/3333 Error: Target not halted Error: auto_probe failed Error: Connect failed. Consider setting up a gdb-attach event for the target to prepare target for GDB connect, or use 'gdb_memory_map disable'. Error: attempted 'gdb' connection rejected

You need to log into the OpenOCD commandline interface and manually halt the chip as shown below. I'm not sure why OpenOCD cannot just do this by default upon an incoming GDB connecton.

$ telnet localhost 4444 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Open On-Chip Debugger > halt target halted due to debug-request, current mode: Thread xPSR: 0x81000000 pc: 0x000001ca msp: 0x10000fe8

RS232 communications

RS232 (UART refers to hardware) even today is a pretty common communications protocol that is mainly used for external communications between electronic devices. For communications within circuits I2C is a better choice, but had I not found out about the latter early on I probably would have used RS232 for intra-circuit communications. Note that connecting these circuit pins to a PC has to be done via an RS232 driver to convert the voltage levels, but the latter is beyond the scope of this article. The table below summarises the registers relevant to UART usage:

Register Bits
Address DLAB Name 0123 4567 8-31
0x40008000 0 Data buffer Data to read/write reserved
1 Divisor LSB Lower byte for UART calculation
0x40008004 0 Interrupt enable Recv data THRE Rx line reserved See P202
0x40008004 1 Divisor MSB Upper byte for UART calculation reserved
0x40008008 n/a Interrupts pending (read) Status Interrupt identity reserved FIFO enable See P202
FIFO control (write) Enable Rx reset Tx reset reserved Rx trigger count reserved
0x4000800c Line Control Data bits Stop bits Parity Parity bits Break DLAB
0x40008010 Modem control DTR RTS reserved LMS reserved RTS en. CTS en.
0x40008014 Line status Recv Overrun Parity error Frame error Break Rx empty Rx error
0x40008018 Modem status DCTS DDSR TERI DDCD CTS DSR Ring DCD
0x4000801c Scratch space Temporary storage for application use
0x40008020 Auto-BAUD control Start Mode Autorestart reserved See P213
0x40008024 reserved reserved
0x40008028 Fraction divider DivAddVal MulVal
0x4000802c reserved
0x40008030 Transmit enable reserved Enable
... reserved
0x4000804c RS485 control Multidrop Rx AAD SEL DCTRL Polarity reserved
0x40008050 RS485 addess RS485 address match
0x40008054 RS485 direction RS485 direction delay

To avoid the table getting excessively wide I have omitted documentation for bits outside the lowest byte, which in all cases where such bits are not reserved they all relate to Auto-BAUD detection. There are quite a few odd choices in the UART registers, such as the sharing of register addresses based on the DLAB bit and the inclusion of the inert Scratch space register, but most of the complexity such as the modem control/status bits are historic. Calculating BAUD parameters is a bit tricky though — for this I wrote a Python script based on what I read in the NXP user guide. The table below shows some common-case parameters I obtain via running this script for the reset-default 12MHz IRC clock, using a UART clock divider value of one:

Target BAUD rate FRest DLM DLL DivAddVal MulVal
9,600 1.5 0 52 1 2
14,400 1.625 0 32 5 8
32,400 1.1 0 21 1 10
57,600 1.3 0 10 3 10
115,200 1.3 0 5 3 10
200,000 1.25 0 3 1 4
300,000 1.25 0 2 1 4

For each BAUD rate there are multiple possibilities, but I have tended to opt for the ones where Frest has had few decimal places, and ideally closer to .5 than .0, given similar computational error. Once setup for the typical use-case of straightforward reading/writing at a known BAUD rate and transmission parameters, UART access is actually extremely simple — my RS232 demo, which mirrors incoming bytes back to the sender, has only ~3 lines of code that are not setup-related. The UART has the same pit-fall as I2C with regards to clocking the I/O Configuration block.

I2C interfacing

For me I2C support is a check-list requirement — I would most likely only use an ARM chip for central processing, with lower-level operations off-loaded onto PIC microcontrollers, and I2C is my interface of choice for communication between integrated circuit chips. The section on I2C within the LPC111x user manual weighs in at 43 pages, but getting very basic I2C functionality working is actually less complex than RS232 — most notably the calculation for the I2C duty cycle is simple compared to the UART BAUD rate calculation, with very few configuration parameters that need to be shared with external devices. The only pit-fall is that the I/O Configuration Block itself needs to be clocked, which caught me out for several days as it is not explicitly mentioned within either the NXP user guide I2C section or the code samples I came across via Google. The table below details the I2C registers:

Address Description
Name Effect when Low Effect when High
0x40000000 Control Set none Set control bit
0x40000018 Control Clear none Clear control bit
0x40000004 Status I2C status code
0x40000008 Data Data payload in/out
0x4000002c Data Buf Read-only copy of inbound data
0x4000000c Slave Address 0 Slave address bits
See Address Mask
0x40000020 Slave Address 1
0x40000024 Slave Address 2
0x40000028 Slave Address 3
0x40000010 Duty Cycle Hi Together specify I2C clock rate
See Table 226 (Page 246) in UM10398
0x40000014 Duty Cycle Low
0x40000030 Address Mask 0 Slave address bit is disregarded Slave address bit must match
0x40000034 Address Mask 1
0x40000038 Address Mask 2
0x4000003c Address Mask 3
0x4000001c Monitor Control Not detailed here

The three important registers are the two control registers and the status register, for which the individual bits are detailed in the table below. The reset default has the clock into the I2C subsystem running at 12MHZ, so for 100kHz set the two duty cycle registers to 60. The Data Buf register is intended for monitor mode — something that is beyond this article — where the chip has very little time before the value in the main Data register gets overwritten, but it is made available in other operating modes as well.

Register Bits
Address Name 01234 5678-31
0x40000000 Control Set reserved Assert
Ack
Interrupt Stop Start Enable reserved
0x40000018 Control Clear reserved
0x40000004 Status unused — always zero I2C status reserved

The I2C Stop flag is auto-cleared, which is why bit 4 of Control Clear is reserved. Even though only some of the bits within the Status register are actual status the user documentation assumes the register's bottom byte is used directly rather than the status bits being shifted downwards, which is why the status codes start at 0x08.

My I2C demo implements a bare-bones write-only master, because getting to this stage of the I2C functionality showing signs of life is by far the highest hurdle. Implementing master reads is just a case of adding in handling of extra states, which the NXP user guide conveniently summarises in a few tables between pages 260 thru 272. At some point I will probably add slave read and write demos, but for the time being I don’t foresee any use-case for having an ARM chips as a slave device. The demo assumed an TCA9535 I/O Expander as a slave device, simply because it was something I had at hand to test with. An existing background with I2C had been assumed as this article is not intended to be a primer on I2C

GCC errors regarding hard floats

For some reason some distributuions of GCC default to hard floats and then complain — as shown below — if the target hardware does not support them. This caught me out as the Windows distributions seems to default to soft floats, which I used for a lot of the development, whereas the SlackBuild ones default to hard. At first I incorrectly assumed this was down to different compiler versions.

gpio.c: In function 'ARMirq': gpio.c:31:6: sorry, unimplemented: Thumb-1 hard-float VFP ABI void ARMirq(uint32_t value) ^~~~~~

The solution is to add -mfloat-abi=soft to the makefile, although I suspect it is not the only commandline parameter that can fix the problem.

Unresponsive UART or I2C

A frustrating part of getting the I2C code working is that all the registers mentioned in the relevant user manual section were being set, but no I2C-related interrupts were being generated and the SI flag was never been set. Various Google searches for terms like “NXP LPC11xx I2C SI not set” mostly pointed to NXP’s forum posts that never mentioned any actual solution, and all the sample code I found did not seem to be significantly different to what my own code was doing. In the end the solution was simple: The I/O Configuration Block itself needs to be clocked:

*((volatile uint32_t *)0x40048080) |= 1 << 16

The same issue also affects the UART, but before finding out about the above I assumed problems there were due to electrical issues, so I put RS232 aside and tried to get I2C working instead. It was only a chance sighting of code that explicitly activated the configuration block and the I2C clocks together that I realised what was wrong — aside from the line in Table 21 (page 34) mentioning the configuration block clock, I have so far seen no hint anywhere that a configuration sub-system would be disabled by default.

Remarks

While figuring things out I had come across references to the Cortex Microcontroller Software Interface Standard (aka CMSIS) in chipset documentation, but I never seriously considered using it — it was only after I had written my own mini-abstraction layer that I even looked at CMSIS and found out that it is a cross-vendor open-source project, by which point it was of no value at all to me. In hindsight I most likely would have avoided it anyway, as its multi-chipset nature means I would have to dig through a load of bloat, and many of the bits that would have been of interest to me are written in ARM assembly rather than C — for the time being learning ARM assembly is not an interest of mine. By and large programming ARM is much nearer traditional desktop software development than PIC microcontroller programming, so I classify it separately from the electronics projects I have done over the last 18 or so months.

My original motive for getting some ARM chips was to learn more about lower-level ARM programming, because I felt that RaspberryPi was too much of a mini-computer that kept me away from the real grubby bare-metal issues, yet I needed to have experience with getting my fingers dirty with the bare metal of ARM. PIC microcontrollers have served me well, but they are ill-suited for much beyond shifting of bits around in a mostly fixed manner, and are a complete pain for anything approaching structured data. From recent experience I have concluded that the PIC architecture is a pretty unsuitable target for C compilers — in many cases at least a measure of inline assembly is needed, and I am tending towards the opinion that one is better off simply writing entire PIC firmware in assembly. The PIC chips with 1kByte of data memory look like a potential ordeal to use, so this is where ARM comes in.