Peripherals

Peripheral Clock Gating

As usual, peripheral clock gating is still handled by driver itself, users don’t need to take care of the peripheral module clock gating.

However, for advanced users who implement their own drivers based on hal and soc components, the previous clock gating include path has been changed from driver/periph_ctrl.h to esp_private/periph_ctrl.h.

RTC Subsystem Control

RTC control APIs have been moved from driver/rtc_cntl.h to esp_private/rtc_ctrl.h.

ADC

  • ADC oneshot mode driver has been redesigned. New driver is in esp_adc component and the include path is esp_adc/adc_oneshot.h. Legacy driver is still available in the previous include path driver/adc.h. However, by default, including driver/adc.h will bring a build warning like legacy adc driver is deprecated, please migrate to use esp_adc/adc_oneshot.h and esp_adc/adc_continuous.h for oneshot mode and continuous mode drivers respectively. The warning can be suppressed by the Kconfig option CONFIG_ADC_SUPPRESS_DEPRECATE_WARN.

  • ADC continuous mode driver has been moved from driver component to esp_adc component. Include path has been changed from driver/adc.h to esp_adc/adc_continuous.h. Legacy driver is still available in the previous include path driver/adc.h. Similarly, including it will bring a build warning, and it can be suppressed by the Kconfig option CONFIG_ADC_SUPPRESS_DEPRECATE_WARN.

  • ADC calibration driver has been redesigned. New driver is in esp_adc component and the include path is esp_adc/adc_cali.h and esp_adc/adc_cali_scheme.h. Legacy driver is still available by including esp_adc_cal.h. However, by default, including esp_adc_cal.h will bring a build warning like legacy adc calibration driver is deprecated, please migrate to use esp_adc/adc_cali.h and esp_adc/adc_cali_scheme.h. The warning can be suppressed by the Kconfig option CONFIG_ADC_CALI_SUPPRESS_DEPRECATE_WARN.

  • API adc_power_acquire and adc_power_release have been deprecated. These two are used by other drivers to maintain ADC power due to hardware limitation. After this change, ADC power will still be handled by the drivers. However, for users who are interested in this, the include path has been changed from driver/adc.h to esp_private/adc_share_hw_ctrl.h.

  • Previous driver/adc2_wifi_private.h has been moved to esp_private/adc_share_hw_ctrl.h.

  • Enums ADC_UNIT_BOTH, ADC_UNIT_ALTER and ADC_UNIT_MAX in adc_unit_t have been removed.

  • Enum ADC_CHANNEL_MAX in adc_channel_t has been removed. Some channels are not supported on some chips, driver will give a dynamic error if an unsupported channels are used.

  • Enum ADC_ATTEN_MAX has been removed. Some attenuations are not supported on some chips, driver will give a dynamic error if an unsupported attenuation is used.

  • Enum ADC_CONV_UNIT_MAX has been removed. Some convert mode are not supported on some chips, driver will give a dynamic error if an unsupported convert mode is used.

  • API hall_sensor_read on ESP32 has been removed. Hall sensor is no more supported on ESP32.

  • API adc_set_i2s_data_source and adc_i2s_mode_init have been deprecated. Related enum adc_i2s_source_t has been deprecated. Please migrate to use esp_adc/adc_continuous.h.

GPIO

  • The previous Kconfig option RTCIO_SUPPORT_RTC_GPIO_DESC has been removed, thus the rtc_gpio_desc array is unavailable. Please use rtc_io_desc array instead.

  • GPIO interrupt users callbacks should no longer read the GPIO interrupt status register to get the triggered GPIO’s pin number. Users should use the callback argument to determine the GPIO’s pin number instead.

    • Previously, when a GPIO interrupt occurs, the GPIO’s interrupt status register is cleared after calling the user callbacks. Thus, it was possible for users to read the GPIO’s interrupt status register inside the callback to determine which GPIO was triggered.

    • However, clearing the interrupt status after the user callbacks can potentially cause edge-triggered interrupts to be lost. For example, if an edge-triggered interrupt (re)triggers while the user callbacks are being called, that interrupt will be cleared without its registered user callback being handled.

    • Now, the GPIO’s interrupt status register is cleared before invoking the user callbacks. Thus, users can no longer read the GPIO interrupt status register to determine which pin has triggered the interrupt. Instead, users should use the callback argument to pass the pin number.

Sigma-Delta Modulator

The Sigma-Delta Modulator driver has been redesigned into SDM. The new driver implements a factory pattern, where the SDM channels are managed in a pool internally, thus you don’t have to fix a SDM channel to a GPIO manually. All SDM channels can be allocated dynamically. Although it’s recommended to use the new driver APIs, the legacy driver is still available in the previous include path driver/sigmadelta.h. However, by default, including driver/sigmadelta.h will bring a build warning like The legacy sigma-delta driver is deprecated, please use driver/sdm.h. The warning can be suppressed by Kconfig option CONFIG_SDM_SUPPRESS_DEPRECATE_WARN.

The major breaking changes in concept and usage are listed as follows:

Breaking Changes in Concepts

  • SDM channel representation has changed from sigmadelta_channel_t to sdm_channel_handle_t, which is an opaque pointer.

  • SDM channel configurations are stored in sdm_config_t now, instead the previous sigmadelta_config_t.

  • In the legacy driver, you don’t have to set the clock source for SDM channel. But in the new driver, you need to set a proper one in the sdm_config_t::clk_src. The available clock sources are listed in the soc_periph_sdm_clk_src_t.

  • In the legacy driver, you need to set a prescale for the channel, which will reflected into the frequency the modulator output a pulse. In the new driver, you should use sdm_config_t::sample_rate_hz.

Breaking Changes in Usage

  • Channel configuration was done by channel allocation, in sdm_new_channel(). In the new driver, only the duty can be changed at runtime, by sdm_channel_set_duty(). Other parameters like gpio number and prescale are only allowed to set during channel allocation.

  • Before further channel operations, you should enable the channel in advance, by calling sdm_channel_enable(). This function will help to manage some system level services, like Power Management.

Timer Group Driver

Timer Group driver has been redesigned into GPTimer, which aims to unify and simplify the usage of general purpose timer. Although it’s recommended to use the the new driver APIs, the legacy driver is still available in the previous include path driver/timer.h. However, by default, including driver/timer.h will bring a build warning like legacy timer group driver is deprecated, please migrate to driver/gptimer.h. The warning can be suppressed by the Kconfig option CONFIG_GPTIMER_SUPPRESS_DEPRECATE_WARN.

The major breaking changes in concept and usage are listed as follows:

Breaking Changes in Concepts

  • timer_group_t and timer_idx_t which used to identify the hardware timer are removed from user’s code. In the new driver, a timer is represented by gptimer_handle_t.

  • Definition of timer source clock is moved to gptimer_clock_source_t, the previous timer_src_clk_t is not used.

  • Definition of timer count direction is moved to gptimer_count_direction_t, the previous timer_count_dir_t is not used.

  • Only level interrupt is supported, timer_intr_t and timer_intr_mode_t are not used.

  • Auto-reload is enabled by set the gptimer_alarm_config_t::auto_reload_on_alarm flag. timer_autoreload_t is not used.

Breaking Changes in Usage

  • Timer initialization is done by creating a timer instance from gptimer_new_timer(). Basic configurations like clock source, resolution and direction should be set in gptimer_config_t. Note that, alarm event specific configurations are not needed during the driver install stage.

  • Alarm event is configured by gptimer_set_alarm_action(), with parameters set in the gptimer_alarm_config_t.

  • Setting and getting count value are done by gptimer_get_raw_count() and gptimer_set_raw_count(). The driver doesn’t help convert the raw value into UTC time-stamp. Instead, the conversion should be done form user’s side as the timer resolution is also known to the user.

  • The driver will install the interrupt service as well if gptimer_event_callbacks_t::on_alarm is set to a valid callback function. In the callback, user doesn’t have to deal with the low level registers (like “clear interrupt status”, “re-enable alarm event” and so on). So functions like timer_group_get_intr_status_in_isr and timer_group_get_auto_reload_in_isr are not used anymore.

  • To update the alarm configurations when alarm event happens, one can call gptimer_set_alarm_action() in the interrupt callback, then the alarm will be re-enabled again.

  • Alarm will always be re-enabled by the driver if gptimer_alarm_config_t::auto_reload_on_alarm is set to true.

UART

Removed/Deprecated items

Replacement

Remarks

uart_isr_register()

None

UART interrupt handling is implemented by driver itself.

uart_isr_free()

None

UART interrupt handling is implemented by driver itself.

use_ref_tick in uart_config_t

uart_config_t::source_clk

Select the clock source.

uart_enable_pattern_det_intr()

uart_enable_pattern_det_baud_intr()

Enable pattern detection interrupt.

I2C

Removed/Deprecated items

Replacement

Remarks

i2c_isr_register()

None

I2C interrupt handling is implemented by driver itself.

i2c_isr_register()

None

I2C interrupt handling is implemented by driver itself.

i2c_opmode_t

None

It’s not used anywhere in esp-idf.

SPI

Removed/Deprecated items

Replacement

Remarks

spi_cal_clock()

spi_get_actual_clock()

Get SPI real working frequency.

  • The internal header file spi_common_internal.h has been moved to esp_private/spi_common_internal.h.

LEDC

Removed/Deprecated items

Replacement

Remarks

bit_num in ledc_timer_config_t

ledc_timer_config_t::duty_resolution

Set resolution of the duty cycle.

Temperature Sensor Driver

  • Old API header temp_sensor.h has been redesigned as temperature_sensor.h, it is recommended to use the new driver and the old driver is not allowed to be used at the same time.

  • Although it’s recommended to use the new driver APIs, the legacy driver is still available in the previous include path driver/temp_sensor.h. However, by default, including driver/temp_sensor.h will bring a build warning like “legacy temperature sensor driver is deprecated, please migrate to driver/temperature_sensor.h”. The warning can be suppressed by enabling the menuconfig option CONFIG_TEMP_SENSOR_SUPPRESS_DEPRECATE_WARN.

  • Configuration contents has been changed. In old version, user need to configure the clk_div and dac_offset. While in new version, user only need to choose tsens_range

  • The process of using temperature sensor has been changed. In old version, user can use config->start->read_celsius to get value. In the new version, user must install the temperature sensor driver firstly, by temperature_sensor_install and uninstall it when finished. For more information, you can refer to Temperature Sensor .

RMT Driver

RMT driver has been redesigned (see RMT transceiver), which aims to unify and extend the usage of RMT peripheral. Although it’s recommended to use the new driver APIs, the legacy driver is still available in the previous include path driver/rmt.h. However, by default, including driver/rmt.h will bring a build warning like The legacy RMT driver is deprecated, please use driver/rmt_tx.h and/or driver/rmt_rx.h. The warning can be suppressed by the Kconfig option CONFIG_RMT_SUPPRESS_DEPRECATE_WARN.

The major breaking changes in concept and usage are listed as follows:

Breaking Changes in Concepts

  • rmt_channel_t which used to identify the hardware channel are removed from user space. In the new driver, RMT channel is represented by rmt_channel_handle_t. The channel is dynamic allocated by the driver, instead of designated by user.

  • rmt_item32_t is replaced by rmt_symbol_word_t, which avoids a nested union inside a struct.

  • rmt_mem_t is removed, as we don’t allow users to access RMT memory block (a.k.an RMTMEM) directly. Direct access to RMTMEM doesn’t make sense but make mistakes, especially when the RMT channel also connected with a DMA channel.

  • rmt_mem_owner_t is removed, as the ownership is controller by driver, not by user anymore.

  • rmt_source_clk_t is replaced by rmt_clock_source_t, note they’re not binary compatible.

  • rmt_data_mode_t is removed, the RMT memory access mode is configured to always use Non-FIFO and DMA mode.

  • rmt_mode_t is removed, as the driver has stand alone install functions for TX and RX channels.

  • rmt_idle_level_t is removed, setting IDLE level for TX channel is available in rmt_transmit_config_t::eot_level.

  • rmt_carrier_level_t is removed, setting carrier polarity is available in rmt_carrier_config_t::polarity_active_low.

  • rmt_channel_status_t and rmt_channel_status_result_t are removed, they’re not used anywhere.

  • transmitting by RMT channel doesn’t expect user to prepare the RMT symbols, instead, user needs to provide an RMT Encoder to tell the driver how to convert user data into RMT symbols.

Breaking Changes in Usage

  • Channel installation has been separated for TX and RX channels into rmt_new_tx_channel() and rmt_new_rx_channel().

  • rmt_set_clk_div and rmt_get_clk_div are removed. Channel clock configuration can only be done during channel installation.

  • rmt_set_rx_idle_thresh and rmt_get_rx_idle_thresh are removed. In the new driver, the RX channel IDLE threshold is redesigned into a new concept rmt_receive_config_t::signal_range_max_ns.

  • rmt_set_mem_block_num and rmt_get_mem_block_num are removed. In the new driver, the memory block number is determined by rmt_tx_channel_config_t::mem_block_symbols and rmt_rx_channel_config_t::mem_block_symbols.

  • rmt_set_tx_carrier is removed, the new driver uses rmt_apply_carrier() to set carrier behavior.

  • rmt_set_mem_pd and rmt_get_mem_pd are removed. The memory power is managed by the driver automatically.

  • rmt_memory_rw_rst, rmt_tx_memory_reset and rmt_rx_memory_reset are removed. Memory reset is managed by the driver automatically.

  • rmt_tx_start and rmt_rx_start are merged into a single function rmt_enable(), for both TX and RX channels.

  • rmt_tx_stop and rmt_rx_stop are merged into a single function rmt_disable(), for both TX and RX channels.

  • rmt_set_memory_owner and rmt_get_memory_owner are removed. RMT memory owner guard is added automatically by the driver.

  • rmt_set_tx_loop_mode and rmt_get_tx_loop_mode are removed. In the new driver, the loop mode is configured in rmt_transmit_config_t::loop_count.

  • rmt_set_source_clk and rmt_get_source_clk are removed. Configuring clock source is only possible during channel installation by rmt_tx_channel_config_t::clk_src and rmt_rx_channel_config_t::clk_src.

  • rmt_set_rx_filter is removed. In the new driver, the filter threshold is redesigned into a new concept rmt_receive_config_t::signal_range_min_ns.

  • rmt_set_idle_level and rmt_get_idle_level are removed. Setting IDLE level for TX channel is available in rmt_transmit_config_t::eot_level.

  • rmt_set_rx_intr_en, rmt_set_err_intr_en, rmt_set_tx_intr_en, rmt_set_tx_thr_intr_en and rmt_set_rx_thr_intr_en are removed. The new driver doesn’t allow user to turn on/off interrupt from user space. Instead, it provides callback functions.

  • rmt_set_gpio and rmt_set_pin are removed. The new driver doesn’t support to switch GPIO dynamically at runtime.

  • rmt_config is removed. In the new driver, basic configuration is done during the channel installation stage.

  • rmt_isr_register and rmt_isr_deregister are removed, the interrupt is allocated by the driver itself.

  • rmt_driver_install is replaced by rmt_new_tx_channel() and rmt_new_rx_channel().

  • rmt_driver_uninstall is replaced by rmt_del_channel().

  • rmt_fill_tx_items, rmt_write_items and rmt_write_sample are removed. In the new driver, user needs to provide an encoder to “translate” the user data into RMT symbols.

  • rmt_get_counter_clock is removed, as the channel clock resolution is configured by user from rmt_tx_channel_config_t::resolution_hz.

  • rmt_wait_tx_done is replaced by rmt_tx_wait_all_done().

  • rmt_translator_init, rmt_translator_set_context and rmt_translator_get_context are removed. In the new driver, the translator has been replaced by the RMT encoder.

  • rmt_get_ringbuf_handle is removed. The new driver doesn’t use Ringbuffer to save RMT symbols. Instead, the incoming data are saved to the user provided buffer directly. The user buffer can even be mounted to DMA link internally.

  • rmt_register_tx_end_callback is replaced by rmt_tx_register_event_callbacks(), where user can register rmt_tx_event_callbacks_t::on_trans_done event callback.

  • rmt_set_intr_enable_mask and rmt_clr_intr_enable_mask are removed, as the interrupt is handled by the driver, user doesn’t need to take care of it.

  • rmt_add_channel_to_group and rmt_remove_channel_from_group are replaced by RMT sync manager. Please refer to rmt_new_sync_manager().

  • rmt_set_tx_loop_count is removed. The loop count in the new driver is configured in rmt_transmit_config_t::loop_count.

  • rmt_enable_tx_loop_autostop is removed. In the new driver, TX loop auto stop is always enabled if available, it’s not configurable anymore.

LCD

  • The LCD panel initialization flow is slightly changed. Now the esp_lcd_panel_init() won’t turn on the display automatically. User needs to call esp_lcd_panel_disp_on_off() to manually turn on the display. Note, this is different from turning on backlight. With this breaking change, user can flush a predefined pattern to the screen before turning on the screen. This can help avoid random noise on the screen after a power on reset.

  • esp_lcd_panel_disp_off() is deprecated, please use esp_lcd_panel_disp_on_off() instead.

  • dc_as_cmd_phase is removed. The SPI LCD driver currently doesn’t support a 9bit SPI LCD. Please always use a dedicated GPIO to control the LCD D/C line.

  • The way to register RGB panel event callbacks has been moved from the esp_lcd_rgb_panel_config_t into a separate API esp_lcd_rgb_panel_register_event_callbacks(). However, the event callback signature is not changed.

  • Previous relax_on_idle flag in esp_lcd_rgb_panel_config_t has been renamed into esp_lcd_rgb_panel_config_t::refresh_on_demand, which expresses the same meaning but with a clear name.

  • If the RGB LCD is created with the refresh_on_demand flag enabled, the driver won’t start a refresh in the esp_lcd_panel_draw_bitmap(). Now you have to call esp_lcd_rgb_panel_refresh() to refresh the screen by yourself.

  • esp_lcd_color_space_t is deprecated, please use lcd_color_space_t to describe the color space, and use lcd_color_rgb_endian_t to describe the data order of RGB color.

Dedicated GPIO Driver

  • All of the dedicated GPIO related LL functionsn in cpu_ll.h have been moved to dedic_gpio_cpu_ll.h and renamed.

I2S driver

Shortcomings are exposed when supporting all the new features of ESP32-C3 & ESP32-S3 by the old I2S driver, so it is re-designed to make it more compatible and flexible to all the communication modes. New APIs are available by including corresponding mode header files driver/include/driver/i2s_std.h, driver/include/driver/i2s_pdm.h or driver/include/driver/i2s_tdm.h. Meanwhile, the old APIs in driver/deprecated/driver/i2s.h are still supported for backward compatibility. But there will be warnings if you keep using the old APIs in your project, these warnings can be suppressed by the Kconfig option CONFIG_I2S_SUPPRESS_DEPRECATE_WARN. Here is the general overview of the current I2S files:

I2S File Structure

Breaking changes in Concepts

  • The minimum control unit in new I2S driver will be tx/rx channel instead of a whole I2S controller.

    1. The tx/rx channel in a same I2S controller can be controlled separately, that means they will be initialized, started or stopped separately. Especially for ESP32-C3 and ESP32-S3, tx and rx channels in one controller can be configured to different clocks or modes now, they are able to work in a totally separate way which can help to save the resources of I2S controller. But for ESP32 and ESP32-S2, though their tx/rx can be controlled separately, some hardware resources are still shared by tx and rx, they might affect each other if they are configured to different configurations;

    2. The channels can be registered to an available I2S controller automatically by setting i2s_port_t::I2S_NUM_AUTO as I2S port id. The driver will help you to search for the available tx/rx channel. Of cause, driver can still support to be installed by a specific port;

    3. i2s_chan_handle_t is the handle that used for identifying the I2S channels. All the APIs will require the channel handle, users need to maintain the channel handles by themselves;

    4. In order to distinguish tx/rx channel and sound channel, now the word ‘channel’ is only stand for the tx/rx channel in new driver, meanwhile the sound channel will be called ‘slot’.

  • I2S communication modes are extracted into three modes.

    1. Standard mode: Standard mode always has two slots, it can support Philips, MSB and PCM(short sync) format, please refer to driver/include/driver/i2s_std.h for details;

    2. PDM mode: PDM mode only support two slots with 16 bits data width, but the configurations of PDM TX and PDM RX are little bit different. For PDM TX, the sample rate can be set by i2s_pdm_tx_clk_config_t::sample_rate, and its clock frequency is depended on the up-sampling configuration. For PDM RX, the sample rate can be set by i2s_pdm_rx_clk_config_t::sample_rate, and its clock frequency is depended on the down-sampling configuration. Please refer to driver/include/driver/i2s_pdm.h for details;

    3. TDM mode: TDM mode can support upto 16 slots. It can work in Philips, MSB, PCM(short sync) and PCM(long sync) format, please refer to driver/include/driver/i2s_tdm.h for details;

    4. When allocating a new channel in a specific mode, must initialize this channel by corresponding function. It is strongly recommended to use the helper macros to generate the default configurations, in case the default values will be changed one day.

  • States and state-machine are adopted in the new I2S driver to avoid APIs called in wrong state.

  • The slot configurations and clock configurations can be configured separately.

    1. Calling i2s_channel_init_std_mode(), i2s_channel_init_pdm_rx_mode(), i2s_channel_init_pdm_tx_mode() or i2s_channel_init_tdm_mode() to initialize the slot/clock/gpio_pin configurations;

    2. Calling i2s_channel_reconfig_std_slot(), i2s_channel_reconfig_pdm_rx_slot(), i2s_channel_reconfig_pdm_tx_slot() or i2s_channel_reconfig_tdm_slot() can change the slot configurations after initialization;

    3. Calling i2s_channel_reconfig_std_clock(), i2s_channel_reconfig_pdm_rx_clock(), i2s_channel_reconfig_pdm_tx_clock() or i2s_channel_reconfig_tdm_clock() can change the clock configurations after initialization;

    4. Calling i2s_channel_reconfig_std_gpio(), i2s_channel_reconfig_pdm_rx_gpio(), i2s_channel_reconfig_pdm_tx_gpio() or i2s_channel_reconfig_tdm_gpio() can change the gpio configurations after initialization.

  • ADC and DAC modes are removed. They will only be supported in their own driver and legacy I2S driver.

  • i2s_channel_write() and i2s_channel_read() can be aborted by i2s_channel_abort_reading_writing() now.

Breaking Changes in Usage

To use the new I2S driver, please follow these steps:

  1. Calling i2s_new_channel() to aquire the channel handles. We should specify the work role and I2S port in this step. Besides, the tx or rx channel handles will be generated by the driver. Inputting both two tx and rx handles is not necessary but at least one handle is needed. In the case of inputting both two handles, the driver will work at duplex mode, both tx and rx channel will be avaliable on a same port, and they will share the MCLK, BCLK and WS signal. But if only one of the tx or rx handle is inputted, this channel will only work in simplex mode.

  2. Calling i2s_channel_init_std_mode(), i2s_channel_init_pdm_rx_mode(), i2s_channel_init_pdm_tx_mode() or i2s_channel_init_tdm_mode() to initialize the channel to the specified mode. Corresponding slot, clock and gpio configurations are needed in this step.

  3. (Optional) Calling i2s_channel_register_event_callback() to register the ISR event callback functions. I2S events now can be received by the callback function synchronously, instead of from event queue asynchronously.

  4. Calling i2s_channel_enable() to start the hardware of I2S channel. In the new driver, I2S won’t start automatically after installed anymore, users are supposed to know clearly whether the channel has started or not.

  5. Reading or writing data by i2s_channel_read() or i2s_channel_write(). Certainly, only rx channel handle is suppoesd to be inputted in i2s_channel_read() and tx channel handle in i2s_channel_write().

  6. (Optional) The slot, clock and gpio configurations can be changed by corresponding ‘reconfig’ functions, but i2s_channel_disable() must be called before updating the configurations.

  7. Calling i2s_channel_disable() to stop the hardware of I2S channel.

  8. Calling i2s_del_channel() to delete and release the resources of the channel if it is not needed any more, but the channel must be disabled before deleting it.

TWAI Driver

The deprecated CAN peripheral driver is removed. Please use TWAI driver instead (i.e. include driver/twai.h in your application).

Register access macros

Previously, all register access macros could be used as expressions, so the following was allowed:

uint32_t val = REG_SET_BITS(reg, mask);

In IDF v5.0, register access macros which write or read-modify-write the register can no longer be used as expressions, and can only be used as statements. This applies to the following macros: REG_WRITE, REG_SET_BIT, REG_CLR_BIT, REG_SET_BITS, REG_SET_FIELD, WRITE_PERI_REG, CLEAR_PERI_REG_MASK, SET_PERI_REG_MASK, SET_PERI_REG_BITS.

To store the value which would have been written into the register, split the operation as follows:

uint32_t new_val = REG_READ(reg) | mask;
REG_WRITE(reg, new_val);

To get the value of the register after modification (which may be different from the value written), add an explicit read:

REG_SET_BITS(reg, mask);
uint32_t new_val = REG_READ(reg);