Skip to content

Modem — AT Commands

The nRF9151 modem is controlled over UART1 (GPIO16–19) with hardware flow control. The board ships with two Python drivers that share the same interface but differ in how they handle waiting for modem responses.

ModuleClassWhen to use
modemModemSimple scripts, no concurrent tasks
modem_uasyncModemAsyncApplications using uasyncio — timers, sensors, networking running concurrently

Both classes are singletons — only one instance exists at runtime. Do not use both in the same application.


The synchronous driver is straightforward: every call blocks until the modem replies or the timeout expires.

from modem import Modem
m = Modem() # initialises nRF9151 and clears the RX buffer

Send a raw AT command and check the response

Section titled “Send a raw AT command and check the response”
ok = m.send_cmd("AT") # returns True if modem replies "OK"
print(ok)
response = m.send_cmd("AT+CGMR", expected="OK", is_bool=False)
print(response) # firmware version string
m.send(b"AT+CFUN=1\r\n")
resp = m.wait_response(expected="OK", timeout_ms=5000)
print(resp)
m.CFUN(1) # full functionality — connects to the network
m.CFUN(4) # flight mode
m.CFUN(0) # minimum functionality / power off RF

Valid CFUN modes: 0, 1, 2, 4, 20, 21, 30, 31, 40, 41, 44.


ModemAsync extends Modem and replaces the blocking I/O layer with uasyncio events. Use it whenever you need the modem to coexist with other concurrent tasks — blinking LEDs, reading sensors, or managing multiple network connections.

from modem_uasync import ModemAsync
import uasyncio
async def main():
m = ModemAsync()
await m.start() # mandatory: launches the background serial reader
...
uasyncio.run(main())

These constraints come from MicroPython’s uasyncio event loop model and apply everywhere in your async code, not just to modem calls.

1. Every method call must be awaited.
Without await, the call returns a coroutine object — no AT command is sent, no value is returned.

# correct
result = await m.mqtt_publish("topic", "payload")
# wrong — AT command is never sent, result is a coroutine object
result = m.mqtt_publish("topic", "payload")

2. await m.start() must be called before anything else.
It creates the _reader() background task that reads incoming bytes from the modem. Without it, every wait_response() will time out.

3. Never use blocking sleep inside a coroutine.
time.sleep() and time.sleep_ms() freeze the entire event loop, preventing _reader() from running and causing spurious timeouts.

# correct
await uasyncio.sleep_ms(1000)
# wrong — blocks the event loop
import time
time.sleep_ms(1000)

4. Long-running loops must be separate tasks.
Use uasyncio.create_task(coro()) to run a coroutine concurrently. Calling await coro() inline will block main() until coro() returns.

# correct — runs concurrently with the rest of main()
uasyncio.create_task(on_message(m))
# wrong — main() waits here forever
await on_message(m)

5. recv() suspends the calling coroutine.
await m.recv() blocks the current coroutine until an MQTT message arrives. Always run it inside a dedicated task.

async def on_message(m):
while True:
topic, payload = await m.recv()
print(topic, payload)
uasyncio.create_task(on_message(m)) # not: await on_message(m)

If you prefer a lower-level approach without the modem module:

from machine import UART, Pin
# Power on the modem
pwr_en = Pin("MODEM_PWR_EN", Pin.OUT, value=1)
pwr_sync = Pin("MODEM_PWR_SYNC", Pin.OUT, value=0)
reset = Pin("MODEM_RESET", Pin.OUT, value=1)
modem = UART(1, baudrate=115200,
tx=Pin("MODEM_TX"), rx=Pin("MODEM_RX"),
cts=Pin("MODEM_CTS"), rts=Pin("MODEM_RTS"))
modem.write(b"AT\r\n")
print(modem.read(64))

See the modem reference overview for links to the full Modem and ModemAsync API documentation.