devicetree: better DT_PROP_BY_IDX()/DT_FOREACH_PROP_ELEM() support

Support use of these macros with properties of type phandle and
string by allowing iterating over:

- a phandle as if it were a phandles of length 1, for convenience and
  consistency with our ability to take its length (and getting 1)

- the non-null characters in a string: we exclude the null for
  consistency with the return value of DT_PROP_LEN() on string
  properties, which, like strlen(), does not include the null

With this and a previous patch expanding the usage of DT_PROP_LEN(),
there is now a relationship between being able to take a property's
logical length with DT_PROP_LEN() and being able to iterate over its
logical elements with DT_FOREACH_PROP_ELEM(). Explain this in the
documentation.

Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
diff --git a/include/zephyr/devicetree.h b/include/zephyr/devicetree.h
index bb0d58c..ed8db9e 100644
--- a/include/zephyr/devicetree.h
+++ b/include/zephyr/devicetree.h
@@ -725,16 +725,28 @@
  * It might help to read the argument order as being similar to
  * `node->property[index]`.
  *
- * When the property's binding has type array, string-array,
- * uint8-array, or phandles, this expands to the idx-th array element
- * as an integer, string literal, or node identifier respectively.
+ * The return value depends on the property's type:
+ *
+ * - for types array, string-array, uint8-array, and phandles,
+ *   this expands to the idx-th array element as an
+ *   integer, string literal, integer, and node identifier
+ *   respectively
+ *
+ * - for type phandle, idx must be 0 and the expansion is a node
+ *   identifier (this treats phandle like a phandles of length 1)
+ *
+ * - for type string, idx must be 0 and the expansion is the the
+ *   entire string (this treats string like string-array of length 1)
  *
  * These properties are handled as special cases:
  *
- * - `reg` property: use DT_REG_ADDR_BY_IDX() or DT_REG_SIZE_BY_IDX() instead
- * - `interrupts` property: use DT_IRQ_BY_IDX() instead
+ * - `reg`: use DT_REG_ADDR_BY_IDX() or DT_REG_SIZE_BY_IDX() instead
+ * - `interrupts`: use DT_IRQ_BY_IDX()
+ * - `ranges`: use DT_NUM_RANGES()
+ * - `dma-ranges`: it is an error to use this property with
+ *   DT_PROP_BY_IDX()
  *
- * For non-array properties, behavior is undefined.
+ * For properties of other types, behavior is undefined.
  *
  * @param node_id node identifier
  * @param prop lowercase-and-underscores property name
@@ -2653,6 +2665,9 @@
  * DT_FOREACH_PROP_ELEM(), and @p idx is the current index into the array.
  * The @p idx values are integer literals starting from 0.
  *
+ * The @p prop argument must refer to a property that can be passed to
+ * DT_PROP_LEN().
+ *
  * Example devicetree fragment:
  *
  * @code{.dts}
@@ -2687,13 +2702,10 @@
  * where `n` is the number of elements in @p prop, as it would be
  * returned by `DT_PROP_LEN(node_id, prop)`.
  *
- * The @p prop argument must refer to a property with type `string`,
- * `array`, `uint8-array`, `string-array`, `phandles`, or `phandle-array`. It
- * is an error to use this macro with properties of other types.
- *
  * @param node_id node identifier
  * @param prop lowercase-and-underscores property name
  * @param fn macro to invoke
+ * @see DT_PROP_LEN
  */
 #define DT_FOREACH_PROP_ELEM(node_id, prop, fn)		\
 	DT_CAT4(node_id, _P_, prop, _FOREACH_PROP_ELEM)(fn)
@@ -2730,9 +2742,8 @@
  *     };
  * @endcode
  *
- * The "prop" argument must refer to a property with type string,
- * array, uint8-array, string-array, phandles, or phandle-array. It is
- * an error to use this macro with properties of other types.
+ * The @p prop parameter has the same restrictions as the same parameter
+ * given to DT_FOREACH_PROP_ELEM().
  *
  * @param node_id node identifier
  * @param prop lowercase-and-underscores property name
@@ -2755,6 +2766,9 @@
  * the array. The @p idx values are integer literals starting from 0. The
  * remaining arguments are passed-in by the caller.
  *
+ * The @p prop parameter has the same restrictions as the same parameter
+ * given to DT_FOREACH_PROP_ELEM().
+ *
  * @param node_id node identifier
  * @param prop lowercase-and-underscores property name
  * @param fn macro to invoke
@@ -2769,6 +2783,9 @@
  * @brief Invokes @p fn for each element in the value of property @p prop with
  * multiple arguments and a separator.
  *
+ * The @p prop parameter has the same restrictions as the same parameter
+ * given to DT_FOREACH_PROP_ELEM().
+ *
  * @param node_id node identifier
  * @param prop lowercase-and-underscores property name
  * @param fn macro to invoke
diff --git a/scripts/dts/gen_defines.py b/scripts/dts/gen_defines.py
index 31a02c0..2394e3a 100755
--- a/scripts/dts/gen_defines.py
+++ b/scripts/dts/gen_defines.py
@@ -32,12 +32,6 @@
 
 from devicetree import edtlib
 
-# The set of binding types whose values can be iterated over with
-# DT_FOREACH_PROP_ELEM(). If you change this, make sure to update the
-# doxygen string for that macro.
-FOREACH_PROP_ELEM_TYPES = set(['string', 'array', 'uint8-array', 'string-array',
-                               'phandles', 'phandle-array'])
-
 class LogFormatter(logging.Formatter):
     '''A log formatter that prints the level name in lower case,
     for compatibility with earlier versions of edtlib.'''
@@ -651,6 +645,12 @@
             macro2val[macro + "_STRING_TOKEN"] = prop.val_as_token
             # DT_N_<node-id>_P_<prop-id>_IDX_<i>_STRING_UPPER_TOKEN
             macro2val[macro + "_STRING_UPPER_TOKEN"] = prop.val_as_token.upper()
+            # DT_N_<node-id>_P_<prop-id>_IDX_0:
+            # DT_N_<node-id>_P_<prop-id>_IDX_0_EXISTS:
+            # Allows treating the string like a degenerate case of a
+            # string-array of length 1.
+            macro2val[macro + "_IDX_0"] = quote_str(prop.val)
+            macro2val[macro + "_IDX_0_EXISTS"] = 1
 
         if prop.enum_index is not None:
             # DT_N_<node-id>_P_<prop-id>_ENUM_IDX
@@ -692,33 +692,32 @@
                     macro2val[macro + f"_IDX_{i}"] = subval
                 macro2val[macro + f"_IDX_{i}_EXISTS"] = 1
 
-        if prop.type in FOREACH_PROP_ELEM_TYPES:
+        plen = prop_len(prop)
+        if plen is not None:
             # DT_N_<node-id>_P_<prop-id>_FOREACH_PROP_ELEM
             macro2val[f"{macro}_FOREACH_PROP_ELEM(fn)"] = \
                 ' \\\n\t'.join(
                     f'fn(DT_{node.z_path_id}, {prop_id}, {i})'
-                    for i in range(len(prop.val)))
+                    for i in range(plen))
 
             # DT_N_<node-id>_P_<prop-id>_FOREACH_PROP_ELEM_SEP
             macro2val[f"{macro}_FOREACH_PROP_ELEM_SEP(fn, sep)"] = \
                 ' DT_DEBRACKET_INTERNAL sep \\\n\t'.join(
                     f'fn(DT_{node.z_path_id}, {prop_id}, {i})'
-                    for i in range(len(prop.val)))
+                    for i in range(plen))
 
             # DT_N_<node-id>_P_<prop-id>_FOREACH_PROP_ELEM_VARGS
             macro2val[f"{macro}_FOREACH_PROP_ELEM_VARGS(fn, ...)"] = \
                 ' \\\n\t'.join(
                     f'fn(DT_{node.z_path_id}, {prop_id}, {i}, __VA_ARGS__)'
-                    for i in range(len(prop.val)))
+                    for i in range(plen))
 
             # DT_N_<node-id>_P_<prop-id>_FOREACH_PROP_ELEM_SEP_VARGS
             macro2val[f"{macro}_FOREACH_PROP_ELEM_SEP_VARGS(fn, sep, ...)"] = \
                 ' DT_DEBRACKET_INTERNAL sep \\\n\t'.join(
                     f'fn(DT_{node.z_path_id}, {prop_id}, {i}, __VA_ARGS__)'
-                    for i in range(len(prop.val)))
+                    for i in range(plen))
 
-        plen = prop_len(prop)
-        if plen is not None:
             # DT_N_<node-id>_P_<prop-id>_LEN
             macro2val[macro + "_LEN"] = plen
 
@@ -786,6 +785,11 @@
     # Returns the property's length if and only if we should generate
     # a _LEN macro for the property. Otherwise, returns None.
     #
+    # The set of types handled here coincides with the allowable types
+    # that can be used with DT_PROP_LEN(). If you change this set,
+    # make sure to update the doxygen string for that macro, and make
+    # sure that DT_FOREACH_PROP_ELEM() works for the new types too.
+    #
     # This deliberately excludes ranges, dma-ranges, reg and interrupts.
     # While they have array type, their lengths as arrays are
     # basically nonsense semantically due to #address-cells and